@quolu/lattice 0.13.0 → 0.15.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/bin/lattice.mjs +5 -0
- package/package.json +2 -2
- package/src/cli-help.mjs +4 -0
- package/src/project-cli.mjs +148 -37
- package/src/seam-proposal-contracts.mjs +427 -0
- package/src/seam-proposal-queries.mjs +484 -0
- package/src/seam-proposal.mjs +1763 -0
- package/src/todo-cli.mjs +285 -9
- package/src/todo-gantt-html.mjs +60 -1
- package/src/todo-gantt-layout.mjs +76 -0
- package/src/todo-independence-contracts.mjs +61 -8
- package/src/todo-independence-guidance.mjs +49 -0
- package/src/todo-independence.mjs +71 -10
- package/src/todo-store.mjs +103 -2
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import {
|
|
2
|
+
digestTodoArtifact,
|
|
3
|
+
exactRecord,
|
|
4
|
+
isNonNegativeSafeInteger,
|
|
5
|
+
isStrictTodoTimestamp,
|
|
6
|
+
isTodoDigest,
|
|
7
|
+
isTodoIdentifier,
|
|
8
|
+
todoSelfDigest,
|
|
9
|
+
} from './todo-contracts.mjs';
|
|
10
|
+
import {
|
|
11
|
+
TODO_INDEPENDENCE_CONFLICT_KINDS,
|
|
12
|
+
TODO_INDEPENDENCE_COVERAGE,
|
|
13
|
+
TODO_INDEPENDENCE_SCHEMA,
|
|
14
|
+
TODO_INDEPENDENCE_TASK_LIMIT,
|
|
15
|
+
isGitSha,
|
|
16
|
+
severabilityOfConflictKind,
|
|
17
|
+
} from './todo-independence-contracts.mjs';
|
|
18
|
+
import { SEAM_PROPOSAL_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
19
|
+
|
|
20
|
+
export const SEAM_PROPOSAL_SCHEMA = 'lattice.seam_proposal.v1';
|
|
21
|
+
export const SEAM_PROPOSAL_PROJECTION_SCHEMA = 'lattice.seam_proposal_projection.v1';
|
|
22
|
+
export const SEAM_PROPOSAL_VERDICTS = Object.freeze([
|
|
23
|
+
'seam_candidate',
|
|
24
|
+
'intentional_serial',
|
|
25
|
+
'unknown_requires_evidence',
|
|
26
|
+
]);
|
|
27
|
+
export const SEAM_PROPOSAL_STABLE_SURFACE_ROLES = Object.freeze([
|
|
28
|
+
'composition_test',
|
|
29
|
+
'facade',
|
|
30
|
+
'fixed_oracle',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const LIST_LIMIT = 4_096;
|
|
34
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
35
|
+
const PROPOSAL_ID = /^seam-[0-9a-f]{64}$/u;
|
|
36
|
+
const SURFACE_KINDS = Object.freeze(['path', 'symbol']);
|
|
37
|
+
const STABLE_SURFACE_ROLES = new Set(SEAM_PROPOSAL_STABLE_SURFACE_ROLES);
|
|
38
|
+
|
|
39
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
40
|
+
|
|
41
|
+
function boundedText(value, maxBytes = 4_096) {
|
|
42
|
+
return typeof value === 'string' && value.length > 0 && value === value.trim()
|
|
43
|
+
&& !CONTROL.test(value) && Buffer.byteLength(value) <= maxBytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function boundedList(value, validator, { min = 0, max = LIST_LIMIT } = {}) {
|
|
47
|
+
return Array.isArray(value) && value.length >= min && value.length <= max
|
|
48
|
+
&& value.every(validator);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function strictlySorted(values, key = (value) => value) {
|
|
52
|
+
return values.every((value, index) => index === 0
|
|
53
|
+
|| compareText(key(values[index - 1]), key(value)) < 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function repoRelativePathOrPrefix(value) {
|
|
57
|
+
if (!boundedText(value) || value.startsWith('/') || value.includes('\\')
|
|
58
|
+
|| /^[A-Za-z]:/u.test(value)) return false;
|
|
59
|
+
const body = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
60
|
+
return body.length > 0 && body.split('/')
|
|
61
|
+
.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function repoRelativePath(value) {
|
|
65
|
+
return repoRelativePathOrPrefix(value) && !value.endsWith('/');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sourceBinding(value) {
|
|
69
|
+
return exactRecord(value, [
|
|
70
|
+
'independence_schema', 'independence_result_digest', 'witness_set_digest',
|
|
71
|
+
'plan_version', 'topology_digest', 'base_sha',
|
|
72
|
+
])
|
|
73
|
+
&& value.independence_schema === TODO_INDEPENDENCE_SCHEMA
|
|
74
|
+
&& isTodoDigest(value.independence_result_digest)
|
|
75
|
+
&& isTodoDigest(value.witness_set_digest)
|
|
76
|
+
&& isTodoIdentifier(value.plan_version)
|
|
77
|
+
&& isTodoDigest(value.topology_digest)
|
|
78
|
+
&& isGitSha(value.base_sha);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function taskPair(value, taskIds) {
|
|
82
|
+
return Array.isArray(value) && value.length === 2
|
|
83
|
+
&& value.every(isTodoIdentifier)
|
|
84
|
+
&& compareText(value[0], value[1]) < 0
|
|
85
|
+
&& value.every((taskId) => taskIds.has(taskId));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function conflictEntry(value, taskIds) {
|
|
89
|
+
return exactRecord(value, ['resource_id', 'kind', 'target', 'task_pairs'])
|
|
90
|
+
&& boundedText(value.resource_id)
|
|
91
|
+
&& TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)
|
|
92
|
+
&& (value.kind === 'path'
|
|
93
|
+
? repoRelativePathOrPrefix(value.target)
|
|
94
|
+
: boundedText(value.target))
|
|
95
|
+
&& boundedList(value.task_pairs, (pair) => taskPair(pair, taskIds), { min: 1 })
|
|
96
|
+
&& strictlySorted(value.task_pairs, (pair) => `${pair[0]}\0${pair[1]}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function conflictsFormConnectedComponent(conflicts, taskIds) {
|
|
100
|
+
const adjacency = new Map([...taskIds].map((taskId) => [taskId, new Set()]));
|
|
101
|
+
for (const { task_pairs: pairs } of conflicts) {
|
|
102
|
+
for (const [left, right] of pairs) {
|
|
103
|
+
adjacency.get(left).add(right);
|
|
104
|
+
adjacency.get(right).add(left);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const first = taskIds.values().next().value;
|
|
108
|
+
const visited = new Set([first]);
|
|
109
|
+
const pending = [first];
|
|
110
|
+
while (pending.length > 0) {
|
|
111
|
+
for (const adjacent of adjacency.get(pending.pop())) {
|
|
112
|
+
if (!visited.has(adjacent)) {
|
|
113
|
+
visited.add(adjacent);
|
|
114
|
+
pending.push(adjacent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return visited.size === taskIds.size;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const surfaceKey = (value) => `${value.kind}\0${value.target}\0${value.path}\0${value.role}`;
|
|
122
|
+
|
|
123
|
+
function surfaceEntry(value, taskIds, { proposed }) {
|
|
124
|
+
if (!exactRecord(value, ['kind', 'target', 'path', 'role', 'owner_task_ids'])
|
|
125
|
+
|| !SURFACE_KINDS.includes(value.kind)
|
|
126
|
+
|| !repoRelativePathOrPrefix(value.path)
|
|
127
|
+
|| !boundedText(value.target)
|
|
128
|
+
|| !isTodoIdentifier(value.role)
|
|
129
|
+
|| !boundedList(value.owner_task_ids, isTodoIdentifier, {
|
|
130
|
+
max: proposed ? 1 : TODO_INDEPENDENCE_TASK_LIMIT,
|
|
131
|
+
})
|
|
132
|
+
|| !strictlySorted(value.owner_task_ids)
|
|
133
|
+
|| !value.owner_task_ids.every((taskId) => taskIds.has(taskId))) return false;
|
|
134
|
+
if (value.kind === 'path' && value.target !== value.path) return false;
|
|
135
|
+
if (value.kind === 'symbol' && !repoRelativePath(value.path)) return false;
|
|
136
|
+
if (!proposed) return true;
|
|
137
|
+
return STABLE_SURFACE_ROLES.has(value.role)
|
|
138
|
+
? value.owner_task_ids.length === 0
|
|
139
|
+
: value.owner_task_ids.length === 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function queryEntry(value) {
|
|
143
|
+
return exactRecord(value, [
|
|
144
|
+
'query_id', 'operation', 'target', 'outcome', 'resolved_name', 'resolved_path',
|
|
145
|
+
'result_digest',
|
|
146
|
+
])
|
|
147
|
+
&& isTodoIdentifier(value.query_id)
|
|
148
|
+
&& isTodoIdentifier(value.operation)
|
|
149
|
+
&& boundedText(value.target)
|
|
150
|
+
&& isTodoIdentifier(value.outcome)
|
|
151
|
+
&& (value.resolved_name === null || boundedText(value.resolved_name))
|
|
152
|
+
&& (value.resolved_path === null || repoRelativePath(value.resolved_path))
|
|
153
|
+
&& isTodoDigest(value.result_digest);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `query_set_digest` is an external binding to the exact sensor query set input used for
|
|
158
|
+
* collection (the same meaning as `digestArtifact(request.sensor_query_set)` in the runtime
|
|
159
|
+
* front end). That input is not embedded here, so this validator can check only digest shape.
|
|
160
|
+
* `evidence_digest` separately self-seals this record, including that binding and every receipt.
|
|
161
|
+
*/
|
|
162
|
+
function evidenceEntry(value) {
|
|
163
|
+
return exactRecord(value, ['query_set_digest', 'evidence_digest', 'queries'])
|
|
164
|
+
&& isTodoDigest(value.query_set_digest)
|
|
165
|
+
&& isTodoDigest(value.evidence_digest)
|
|
166
|
+
&& boundedList(value.queries, queryEntry)
|
|
167
|
+
&& strictlySorted(value.queries, (query) => query.query_id)
|
|
168
|
+
&& value.evidence_digest === todoSelfDigest(value, 'evidence_digest');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function verificationEntry(value) {
|
|
172
|
+
return exactRecord(value, [
|
|
173
|
+
'virtual_compile_input_digest', 'virtual_compile_result_digest', 'residual_conflicts',
|
|
174
|
+
])
|
|
175
|
+
&& isTodoDigest(value.virtual_compile_input_digest)
|
|
176
|
+
&& isTodoDigest(value.virtual_compile_result_digest)
|
|
177
|
+
&& Array.isArray(value.residual_conflicts)
|
|
178
|
+
&& value.residual_conflicts.length === 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeConflictIdentity(conflicts) {
|
|
182
|
+
return conflicts.map(({ resource_id: resourceId, kind, target, task_pairs: taskPairs }) => ({
|
|
183
|
+
resource_id: resourceId,
|
|
184
|
+
kind,
|
|
185
|
+
target,
|
|
186
|
+
task_pairs: taskPairs.map(([left, right]) => [left, right])
|
|
187
|
+
.sort((left, right) => compareText(`${left[0]}\0${left[1]}`, `${right[0]}\0${right[1]}`)),
|
|
188
|
+
})).sort((left, right) => compareText(left.resource_id, right.resource_id));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeProposedOwnership(surfaces) {
|
|
192
|
+
return surfaces.map(({ kind, target, path, role, owner_task_ids: ownerTaskIds }) => ({
|
|
193
|
+
kind,
|
|
194
|
+
target,
|
|
195
|
+
path,
|
|
196
|
+
role,
|
|
197
|
+
owner_task_ids: [...ownerTaskIds].sort(compareText),
|
|
198
|
+
})).sort((left, right) => compareText(surfaceKey(left), surfaceKey(right)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Content receipts and source freshness deliberately do not participate in semantic identity.
|
|
203
|
+
* The caller still has to pass the same conflict component and proposed ownership shape.
|
|
204
|
+
*/
|
|
205
|
+
export function deriveSeamProposalId({ conflicts, proposed_surfaces: proposedSurfaces }) {
|
|
206
|
+
const semanticKey = {
|
|
207
|
+
conflicts: normalizeConflictIdentity(conflicts),
|
|
208
|
+
proposed_surfaces: normalizeProposedOwnership(proposedSurfaces),
|
|
209
|
+
};
|
|
210
|
+
return `seam-${digestTodoArtifact(semanticKey)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function seamCandidate(value, conflicts, taskIds) {
|
|
214
|
+
if (!exactRecord(value, [
|
|
215
|
+
'proposal_id', 'current_surfaces', 'proposed_surfaces', 'affected_tests',
|
|
216
|
+
'verification', 'evidence', 'limits', 'proposal_digest',
|
|
217
|
+
])
|
|
218
|
+
|| !PROPOSAL_ID.test(value.proposal_id)
|
|
219
|
+
|| !boundedList(value.current_surfaces,
|
|
220
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: false }))
|
|
221
|
+
|| !strictlySorted(value.current_surfaces, surfaceKey)
|
|
222
|
+
|| !boundedList(value.proposed_surfaces,
|
|
223
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: true }), { min: 1 })
|
|
224
|
+
|| !strictlySorted(value.proposed_surfaces, surfaceKey)
|
|
225
|
+
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
226
|
+
|| !strictlySorted(value.affected_tests)
|
|
227
|
+
|| !verificationEntry(value.verification)
|
|
228
|
+
|| !evidenceEntry(value.evidence)
|
|
229
|
+
|| !boundedList(value.limits, isTodoIdentifier, { min: 1 })
|
|
230
|
+
|| !strictlySorted(value.limits)
|
|
231
|
+
|| !value.limits.includes('structural_only')
|
|
232
|
+
|| !isTodoDigest(value.proposal_digest)) return false;
|
|
233
|
+
const proposedOwnerTaskIds = new Set(value.proposed_surfaces
|
|
234
|
+
.flatMap(({ owner_task_ids: ownerTaskIds }) => ownerTaskIds));
|
|
235
|
+
if (proposedOwnerTaskIds.size !== taskIds.size
|
|
236
|
+
|| ![...taskIds].every((taskId) => proposedOwnerTaskIds.has(taskId))) return false;
|
|
237
|
+
if (value.proposal_id !== deriveSeamProposalId({
|
|
238
|
+
conflicts,
|
|
239
|
+
proposed_surfaces: value.proposed_surfaces,
|
|
240
|
+
})) return false;
|
|
241
|
+
return value.proposal_digest === todoSelfDigest(value, 'proposal_digest');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function reasonEntry(value) {
|
|
245
|
+
return exactRecord(value, ['code', 'detail'])
|
|
246
|
+
&& isTodoIdentifier(value.code) && boundedText(value.detail);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function unknownEntry(value) {
|
|
250
|
+
return exactRecord(value, ['kind', 'ref'])
|
|
251
|
+
&& isTodoIdentifier(value.kind) && boundedText(value.ref);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function decisionEntry(value) {
|
|
255
|
+
if (!exactRecord(value, [
|
|
256
|
+
'component_id', 'task_ids', 'conflicts', 'verdict', 'seam_candidate',
|
|
257
|
+
'reasons', 'unknowns',
|
|
258
|
+
])
|
|
259
|
+
|| !isTodoIdentifier(value.component_id)
|
|
260
|
+
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
261
|
+
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
262
|
+
})
|
|
263
|
+
|| !strictlySorted(value.task_ids)
|
|
264
|
+
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
265
|
+
|| !boundedList(value.reasons, reasonEntry)
|
|
266
|
+
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
267
|
+
|| !boundedList(value.unknowns, unknownEntry)
|
|
268
|
+
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)) return false;
|
|
269
|
+
const taskIds = new Set(value.task_ids);
|
|
270
|
+
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
271
|
+
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
272
|
+
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)) return false;
|
|
273
|
+
if (value.verdict === 'seam_candidate') {
|
|
274
|
+
return value.unknowns.length === 0
|
|
275
|
+
&& value.conflicts.every(({ kind }) => severabilityOfConflictKind(kind) === 'code_seam')
|
|
276
|
+
&& seamCandidate(value.seam_candidate, value.conflicts, taskIds);
|
|
277
|
+
}
|
|
278
|
+
if (value.seam_candidate !== null) return false;
|
|
279
|
+
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
280
|
+
return value.unknowns.length > 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Validate the immutable public `lattice.seam_proposal.v1` artifact.
|
|
285
|
+
*
|
|
286
|
+
* This validates the artifact's closed runtime shape and canonical relations. Binding the
|
|
287
|
+
* recorded conflicts back to the referenced independence bytes is a consumer responsibility.
|
|
288
|
+
*/
|
|
289
|
+
export function validateSeamProposal(value) {
|
|
290
|
+
try {
|
|
291
|
+
if (!exactRecord(value, [
|
|
292
|
+
'schema', 'project_id', 'plan_key', 'source_binding', 'compiled_at',
|
|
293
|
+
'decisions', 'result_digest',
|
|
294
|
+
])
|
|
295
|
+
|| value.schema !== SEAM_PROPOSAL_SCHEMA
|
|
296
|
+
|| !isTodoIdentifier(value.project_id)
|
|
297
|
+
|| !isTodoIdentifier(value.plan_key)
|
|
298
|
+
|| !sourceBinding(value.source_binding)
|
|
299
|
+
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
300
|
+
|| !boundedList(value.decisions, decisionEntry)
|
|
301
|
+
|| !strictlySorted(value.decisions, (decision) => decision.component_id)
|
|
302
|
+
|| !isTodoDigest(value.result_digest)) return false;
|
|
303
|
+
const resourceIds = value.decisions
|
|
304
|
+
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
305
|
+
if (new Set(resourceIds).size !== resourceIds.length) return false;
|
|
306
|
+
return value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
307
|
+
} catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const PROJECTION_GUIDANCE_BY_COVERAGE = Object.freeze({
|
|
313
|
+
missing: 'seam_proposal_unrecorded',
|
|
314
|
+
stale: 'seam_proposal_stale',
|
|
315
|
+
superseded: 'seam_proposal_superseded',
|
|
316
|
+
verified: 'seam_proposal_verified',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
function projectionGuidance(value, coverage) {
|
|
320
|
+
return exactRecord(value, ['code', 'message', 'next_action'])
|
|
321
|
+
&& SEAM_PROPOSAL_GUIDANCE_CODES.includes(value.code)
|
|
322
|
+
&& value.code === PROJECTION_GUIDANCE_BY_COVERAGE[coverage]
|
|
323
|
+
&& boundedText(value.message)
|
|
324
|
+
&& isTodoIdentifier(value.next_action);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function projectionComponent(value) {
|
|
328
|
+
if (!exactRecord(value, [
|
|
329
|
+
'component_id', 'verdict', 'task_ids', 'conflicts', 'proposed_surfaces',
|
|
330
|
+
'affected_tests', 'limits', 'reasons', 'unknowns',
|
|
331
|
+
])
|
|
332
|
+
|| !isTodoIdentifier(value.component_id)
|
|
333
|
+
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
334
|
+
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
335
|
+
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
336
|
+
})
|
|
337
|
+
|| !strictlySorted(value.task_ids)
|
|
338
|
+
|| !boundedList(value.reasons, reasonEntry)
|
|
339
|
+
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
340
|
+
|| !boundedList(value.unknowns, unknownEntry)
|
|
341
|
+
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)
|
|
342
|
+
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
343
|
+
|| !strictlySorted(value.affected_tests)
|
|
344
|
+
|| !boundedList(value.limits, isTodoIdentifier)
|
|
345
|
+
|| !strictlySorted(value.limits)) return false;
|
|
346
|
+
const taskIds = new Set(value.task_ids);
|
|
347
|
+
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
348
|
+
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
349
|
+
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)
|
|
350
|
+
|| !boundedList(value.proposed_surfaces,
|
|
351
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: true }))
|
|
352
|
+
|| !strictlySorted(value.proposed_surfaces, surfaceKey)) return false;
|
|
353
|
+
if (value.verdict === 'seam_candidate') {
|
|
354
|
+
const ownerTaskIds = new Set(value.proposed_surfaces
|
|
355
|
+
.flatMap(({ owner_task_ids: owners }) => owners));
|
|
356
|
+
return value.unknowns.length === 0
|
|
357
|
+
&& value.proposed_surfaces.length > 0
|
|
358
|
+
&& value.limits.includes('structural_only')
|
|
359
|
+
&& ownerTaskIds.size === taskIds.size
|
|
360
|
+
&& [...taskIds].every((taskId) => ownerTaskIds.has(taskId));
|
|
361
|
+
}
|
|
362
|
+
if (value.proposed_surfaces.length > 0
|
|
363
|
+
|| value.affected_tests.length > 0
|
|
364
|
+
|| value.limits.length > 0) return false;
|
|
365
|
+
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
366
|
+
return value.unknowns.length > 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Validate the read-only lattice.seam_proposal_projection.v1 public CLI projection.
|
|
371
|
+
* The immutable seam proposal artifact contract above remains independent from this read model.
|
|
372
|
+
*/
|
|
373
|
+
export function validateSeamProposalProjection(value) {
|
|
374
|
+
try {
|
|
375
|
+
if (!exactRecord(value, [
|
|
376
|
+
'schema', 'project_id', 'plan_key', 'coverage', 'compiled_base_sha',
|
|
377
|
+
'current_base_sha', 'plan_version', 'topology_digest', 'independence_result_digest',
|
|
378
|
+
'compiled_at', 'guidance', 'component_count', 'conflict_resource_count',
|
|
379
|
+
'components', 'result_digest',
|
|
380
|
+
])
|
|
381
|
+
|| value.schema !== SEAM_PROPOSAL_PROJECTION_SCHEMA
|
|
382
|
+
|| !isTodoIdentifier(value.project_id)
|
|
383
|
+
|| !isTodoIdentifier(value.plan_key)
|
|
384
|
+
|| !TODO_INDEPENDENCE_COVERAGE.includes(value.coverage)
|
|
385
|
+
|| !isGitSha(value.current_base_sha)
|
|
386
|
+
|| !projectionGuidance(value.guidance, value.coverage)
|
|
387
|
+
|| !isTodoDigest(value.result_digest)) return false;
|
|
388
|
+
|
|
389
|
+
if (value.coverage === 'missing') {
|
|
390
|
+
return value.compiled_base_sha === null
|
|
391
|
+
&& value.plan_version === null
|
|
392
|
+
&& value.topology_digest === null
|
|
393
|
+
&& value.independence_result_digest === null
|
|
394
|
+
&& value.compiled_at === null
|
|
395
|
+
&& value.component_count === null
|
|
396
|
+
&& value.conflict_resource_count === null
|
|
397
|
+
&& Array.isArray(value.components)
|
|
398
|
+
&& value.components.length === 0
|
|
399
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!isGitSha(value.compiled_base_sha)
|
|
403
|
+
|| !isTodoIdentifier(value.plan_version)
|
|
404
|
+
|| !isTodoDigest(value.topology_digest)
|
|
405
|
+
|| !isTodoDigest(value.independence_result_digest)
|
|
406
|
+
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
407
|
+
|| !isNonNegativeSafeInteger(value.component_count)
|
|
408
|
+
|| !isNonNegativeSafeInteger(value.conflict_resource_count)
|
|
409
|
+
|| !boundedList(value.components, projectionComponent)
|
|
410
|
+
|| !strictlySorted(value.components, (component) => component.component_id)
|
|
411
|
+
|| value.component_count !== value.components.length
|
|
412
|
+
|| value.conflict_resource_count !== value.components
|
|
413
|
+
.reduce((count, component) => count + component.conflicts.length, 0)) return false;
|
|
414
|
+
if (value.coverage === 'verified' && value.compiled_base_sha !== value.current_base_sha) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
if (value.coverage === 'stale' && value.compiled_base_sha === value.current_base_sha) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
const resourceIds = value.components
|
|
421
|
+
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
422
|
+
return new Set(resourceIds).size === resourceIds.length
|
|
423
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|