@quolu/lattice 0.52.2 → 0.52.4
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/package.json +1 -1
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-hub-protocol.mjs +198 -0
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +699 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1506
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -166
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1171
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3143 -3141
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -629
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4360 -4322
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,446 +1,446 @@
|
|
|
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 {
|
|
19
|
-
SEAM_PROPOSAL_GUIDANCE_CODES,
|
|
20
|
-
seamProposalGuidanceCode,
|
|
21
|
-
} from './todo-independence-guidance.mjs';
|
|
22
|
-
|
|
23
|
-
export const SEAM_PROPOSAL_SCHEMA = 'lattice.seam_proposal.v2';
|
|
24
|
-
export const SEAM_PROPOSAL_PROJECTION_SCHEMA = 'lattice.seam_proposal_projection.v1';
|
|
25
|
-
export const SEAM_PROPOSAL_VERDICTS = Object.freeze([
|
|
26
|
-
'seam_candidate',
|
|
27
|
-
'intentional_serial',
|
|
28
|
-
'unknown_requires_evidence',
|
|
29
|
-
]);
|
|
30
|
-
export const SEAM_PROPOSAL_STABLE_SURFACE_ROLES = Object.freeze([
|
|
31
|
-
'composition_test',
|
|
32
|
-
'facade',
|
|
33
|
-
'fixed_oracle',
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
const LIST_LIMIT = 4_096;
|
|
37
|
-
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
38
|
-
const PROPOSAL_ID = /^seam-[0-9a-f]{64}$/u;
|
|
39
|
-
const SURFACE_KINDS = Object.freeze(['path', 'symbol']);
|
|
40
|
-
const STABLE_SURFACE_ROLES = new Set(SEAM_PROPOSAL_STABLE_SURFACE_ROLES);
|
|
41
|
-
|
|
42
|
-
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
43
|
-
|
|
44
|
-
function boundedText(value, maxBytes = 4_096) {
|
|
45
|
-
return typeof value === 'string' && value.length > 0 && value === value.trim()
|
|
46
|
-
&& !CONTROL.test(value) && Buffer.byteLength(value) <= maxBytes;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function boundedList(value, validator, { min = 0, max = LIST_LIMIT } = {}) {
|
|
50
|
-
return Array.isArray(value) && value.length >= min && value.length <= max
|
|
51
|
-
&& value.every(validator);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function strictlySorted(values, key = (value) => value) {
|
|
55
|
-
return values.every((value, index) => index === 0
|
|
56
|
-
|| compareText(key(values[index - 1]), key(value)) < 0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function repoRelativePathOrPrefix(value) {
|
|
60
|
-
if (!boundedText(value) || value.startsWith('/') || value.includes('\\')
|
|
61
|
-
|| /^[A-Za-z]:/u.test(value)) return false;
|
|
62
|
-
const body = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
63
|
-
return body.length > 0 && body.split('/')
|
|
64
|
-
.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function repoRelativePath(value) {
|
|
68
|
-
return repoRelativePathOrPrefix(value) && !value.endsWith('/');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function sourceBinding(value) {
|
|
72
|
-
return exactRecord(value, [
|
|
73
|
-
'independence_schema', 'independence_result_digest', 'witness_set_digest',
|
|
74
|
-
'plan_version', 'topology_digest', 'base_sha',
|
|
75
|
-
])
|
|
76
|
-
&& value.independence_schema === TODO_INDEPENDENCE_SCHEMA
|
|
77
|
-
&& isTodoDigest(value.independence_result_digest)
|
|
78
|
-
&& isTodoDigest(value.witness_set_digest)
|
|
79
|
-
&& isTodoIdentifier(value.plan_version)
|
|
80
|
-
&& isTodoDigest(value.topology_digest)
|
|
81
|
-
&& isGitSha(value.base_sha);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function taskPair(value, taskIds) {
|
|
85
|
-
return Array.isArray(value) && value.length === 2
|
|
86
|
-
&& value.every(isTodoIdentifier)
|
|
87
|
-
&& compareText(value[0], value[1]) < 0
|
|
88
|
-
&& value.every((taskId) => taskIds.has(taskId));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function conflictEntry(value, taskIds) {
|
|
92
|
-
return exactRecord(value, ['resource_id', 'kind', 'target', 'task_pairs'])
|
|
93
|
-
&& boundedText(value.resource_id)
|
|
94
|
-
&& TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)
|
|
95
|
-
&& (value.kind === 'path'
|
|
96
|
-
? repoRelativePathOrPrefix(value.target)
|
|
97
|
-
: boundedText(value.target))
|
|
98
|
-
&& boundedList(value.task_pairs, (pair) => taskPair(pair, taskIds), { min: 1 })
|
|
99
|
-
&& strictlySorted(value.task_pairs, (pair) => `${pair[0]}\0${pair[1]}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function conflictsFormConnectedComponent(conflicts, taskIds) {
|
|
103
|
-
const adjacency = new Map([...taskIds].map((taskId) => [taskId, new Set()]));
|
|
104
|
-
for (const { task_pairs: pairs } of conflicts) {
|
|
105
|
-
for (const [left, right] of pairs) {
|
|
106
|
-
adjacency.get(left).add(right);
|
|
107
|
-
adjacency.get(right).add(left);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const first = taskIds.values().next().value;
|
|
111
|
-
const visited = new Set([first]);
|
|
112
|
-
const pending = [first];
|
|
113
|
-
while (pending.length > 0) {
|
|
114
|
-
for (const adjacent of adjacency.get(pending.pop())) {
|
|
115
|
-
if (!visited.has(adjacent)) {
|
|
116
|
-
visited.add(adjacent);
|
|
117
|
-
pending.push(adjacent);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return visited.size === taskIds.size;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const surfaceKey = (value) => `${value.kind}\0${value.target}\0${value.path}\0${value.role}`;
|
|
125
|
-
|
|
126
|
-
function surfaceEntry(value, taskIds, { proposed }) {
|
|
127
|
-
if (!exactRecord(value, ['kind', 'target', 'path', 'role', 'owner_task_ids'])
|
|
128
|
-
|| !SURFACE_KINDS.includes(value.kind)
|
|
129
|
-
|| !repoRelativePathOrPrefix(value.path)
|
|
130
|
-
|| !boundedText(value.target)
|
|
131
|
-
|| !isTodoIdentifier(value.role)
|
|
132
|
-
|| !boundedList(value.owner_task_ids, isTodoIdentifier, {
|
|
133
|
-
max: proposed ? 1 : TODO_INDEPENDENCE_TASK_LIMIT,
|
|
134
|
-
})
|
|
135
|
-
|| !strictlySorted(value.owner_task_ids)
|
|
136
|
-
|| !value.owner_task_ids.every((taskId) => taskIds.has(taskId))) return false;
|
|
137
|
-
if (value.kind === 'path' && value.target !== value.path) return false;
|
|
138
|
-
if (value.kind === 'symbol' && !repoRelativePath(value.path)) return false;
|
|
139
|
-
if (!proposed) return true;
|
|
140
|
-
return STABLE_SURFACE_ROLES.has(value.role)
|
|
141
|
-
? value.owner_task_ids.length === 0
|
|
142
|
-
: value.owner_task_ids.length === 1;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* `candidate_paths`は、同名symbolが複数fileに居た時だけ埋まる(ADR 0134)。
|
|
147
|
-
*
|
|
148
|
-
* 単数の`resolved_path`と併存させるのは、両者が別の事実だからである。前者は「一意に決まった」、
|
|
149
|
-
* 後者は「決まらず、候補はこれだけあった」。候補を持つ受け皿が無かった頃、後者は`unknown`へ
|
|
150
|
-
* 潰れ、宣言の`within`で絞れる情報が記録に残らなかった。一意に決まった時に候補を並べない
|
|
151
|
-
* (空配列に限る)のは、決まった事実と決まらなかった事実を同じ形にしないためである。
|
|
152
|
-
*/
|
|
153
|
-
function queryEntry(value) {
|
|
154
|
-
return exactRecord(value, [
|
|
155
|
-
'query_id', 'operation', 'target', 'outcome', 'resolved_name', 'resolved_path',
|
|
156
|
-
'candidate_paths', 'result_digest',
|
|
157
|
-
])
|
|
158
|
-
&& isTodoIdentifier(value.query_id)
|
|
159
|
-
&& isTodoIdentifier(value.operation)
|
|
160
|
-
&& boundedText(value.target)
|
|
161
|
-
&& isTodoIdentifier(value.outcome)
|
|
162
|
-
&& (value.resolved_name === null || boundedText(value.resolved_name))
|
|
163
|
-
&& (value.resolved_path === null || repoRelativePath(value.resolved_path))
|
|
164
|
-
&& boundedList(value.candidate_paths, repoRelativePath)
|
|
165
|
-
&& strictlySorted(value.candidate_paths)
|
|
166
|
-
&& (value.outcome === 'ambiguous'
|
|
167
|
-
? value.candidate_paths.length >= 2 && value.resolved_path === null
|
|
168
|
-
&& boundedText(value.resolved_name)
|
|
169
|
-
: value.candidate_paths.length === 0)
|
|
170
|
-
&& isTodoDigest(value.result_digest);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* `query_set_digest` is an external binding to the exact sensor query set input used for
|
|
175
|
-
* collection (the same meaning as `digestArtifact(request.sensor_query_set)` in the runtime
|
|
176
|
-
* front end). That input is not embedded here, so this validator can check only digest shape.
|
|
177
|
-
* `evidence_digest` separately self-seals this record, including that binding and every receipt.
|
|
178
|
-
*/
|
|
179
|
-
function evidenceEntry(value) {
|
|
180
|
-
return exactRecord(value, ['query_set_digest', 'evidence_digest', 'queries'])
|
|
181
|
-
&& isTodoDigest(value.query_set_digest)
|
|
182
|
-
&& isTodoDigest(value.evidence_digest)
|
|
183
|
-
&& boundedList(value.queries, queryEntry)
|
|
184
|
-
&& strictlySorted(value.queries, (query) => query.query_id)
|
|
185
|
-
&& value.evidence_digest === todoSelfDigest(value, 'evidence_digest');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function verificationEntry(value) {
|
|
189
|
-
return exactRecord(value, [
|
|
190
|
-
'virtual_compile_input_digest', 'virtual_compile_result_digest', 'residual_conflicts',
|
|
191
|
-
])
|
|
192
|
-
&& isTodoDigest(value.virtual_compile_input_digest)
|
|
193
|
-
&& isTodoDigest(value.virtual_compile_result_digest)
|
|
194
|
-
&& Array.isArray(value.residual_conflicts)
|
|
195
|
-
&& value.residual_conflicts.length === 0;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function normalizeConflictIdentity(conflicts) {
|
|
199
|
-
return conflicts.map(({ resource_id: resourceId, kind, target, task_pairs: taskPairs }) => ({
|
|
200
|
-
resource_id: resourceId,
|
|
201
|
-
kind,
|
|
202
|
-
target,
|
|
203
|
-
task_pairs: taskPairs.map(([left, right]) => [left, right])
|
|
204
|
-
.sort((left, right) => compareText(`${left[0]}\0${left[1]}`, `${right[0]}\0${right[1]}`)),
|
|
205
|
-
})).sort((left, right) => compareText(left.resource_id, right.resource_id));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function normalizeProposedOwnership(surfaces) {
|
|
209
|
-
return surfaces.map(({ kind, target, path, role, owner_task_ids: ownerTaskIds }) => ({
|
|
210
|
-
kind,
|
|
211
|
-
target,
|
|
212
|
-
path,
|
|
213
|
-
role,
|
|
214
|
-
owner_task_ids: [...ownerTaskIds].sort(compareText),
|
|
215
|
-
})).sort((left, right) => compareText(surfaceKey(left), surfaceKey(right)));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Content receipts and source freshness deliberately do not participate in semantic identity.
|
|
220
|
-
* The caller still has to pass the same conflict component and proposed ownership shape.
|
|
221
|
-
*/
|
|
222
|
-
export function deriveSeamProposalId({ conflicts, proposed_surfaces: proposedSurfaces }) {
|
|
223
|
-
const semanticKey = {
|
|
224
|
-
conflicts: normalizeConflictIdentity(conflicts),
|
|
225
|
-
proposed_surfaces: normalizeProposedOwnership(proposedSurfaces),
|
|
226
|
-
};
|
|
227
|
-
return `seam-${digestTodoArtifact(semanticKey)}`;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function seamCandidate(value, conflicts, taskIds) {
|
|
231
|
-
if (!exactRecord(value, [
|
|
232
|
-
'proposal_id', 'current_surfaces', 'proposed_surfaces', 'affected_tests',
|
|
233
|
-
'verification', 'evidence', 'limits', 'proposal_digest',
|
|
234
|
-
])
|
|
235
|
-
|| !PROPOSAL_ID.test(value.proposal_id)
|
|
236
|
-
|| !boundedList(value.current_surfaces,
|
|
237
|
-
(surface) => surfaceEntry(surface, taskIds, { proposed: false }))
|
|
238
|
-
|| !strictlySorted(value.current_surfaces, surfaceKey)
|
|
239
|
-
|| !boundedList(value.proposed_surfaces,
|
|
240
|
-
(surface) => surfaceEntry(surface, taskIds, { proposed: true }), { min: 1 })
|
|
241
|
-
|| !strictlySorted(value.proposed_surfaces, surfaceKey)
|
|
242
|
-
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
243
|
-
|| !strictlySorted(value.affected_tests)
|
|
244
|
-
|| !verificationEntry(value.verification)
|
|
245
|
-
|| !evidenceEntry(value.evidence)
|
|
246
|
-
|| !boundedList(value.limits, isTodoIdentifier, { min: 1 })
|
|
247
|
-
|| !strictlySorted(value.limits)
|
|
248
|
-
|| !value.limits.includes('structural_only')
|
|
249
|
-
|| !isTodoDigest(value.proposal_digest)) return false;
|
|
250
|
-
const proposedOwnerTaskIds = new Set(value.proposed_surfaces
|
|
251
|
-
.flatMap(({ owner_task_ids: ownerTaskIds }) => ownerTaskIds));
|
|
252
|
-
if (proposedOwnerTaskIds.size !== taskIds.size
|
|
253
|
-
|| ![...taskIds].every((taskId) => proposedOwnerTaskIds.has(taskId))) return false;
|
|
254
|
-
if (value.proposal_id !== deriveSeamProposalId({
|
|
255
|
-
conflicts,
|
|
256
|
-
proposed_surfaces: value.proposed_surfaces,
|
|
257
|
-
})) return false;
|
|
258
|
-
return value.proposal_digest === todoSelfDigest(value, 'proposal_digest');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function reasonEntry(value) {
|
|
262
|
-
return exactRecord(value, ['code', 'detail'])
|
|
263
|
-
&& isTodoIdentifier(value.code) && boundedText(value.detail);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function unknownEntry(value) {
|
|
267
|
-
return exactRecord(value, ['kind', 'ref'])
|
|
268
|
-
&& isTodoIdentifier(value.kind) && boundedText(value.ref);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function decisionEntry(value) {
|
|
272
|
-
if (!exactRecord(value, [
|
|
273
|
-
'component_id', 'task_ids', 'conflicts', 'verdict', 'seam_candidate',
|
|
274
|
-
'reasons', 'unknowns',
|
|
275
|
-
])
|
|
276
|
-
|| !isTodoIdentifier(value.component_id)
|
|
277
|
-
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
278
|
-
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
279
|
-
})
|
|
280
|
-
|| !strictlySorted(value.task_ids)
|
|
281
|
-
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
282
|
-
|| !boundedList(value.reasons, reasonEntry)
|
|
283
|
-
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
284
|
-
|| !boundedList(value.unknowns, unknownEntry)
|
|
285
|
-
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)) return false;
|
|
286
|
-
const taskIds = new Set(value.task_ids);
|
|
287
|
-
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
288
|
-
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
289
|
-
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)) return false;
|
|
290
|
-
if (value.verdict === 'seam_candidate') {
|
|
291
|
-
return value.unknowns.length === 0
|
|
292
|
-
&& value.conflicts.every(({ kind }) => severabilityOfConflictKind(kind) === 'code_seam')
|
|
293
|
-
&& seamCandidate(value.seam_candidate, value.conflicts, taskIds);
|
|
294
|
-
}
|
|
295
|
-
if (value.seam_candidate !== null) return false;
|
|
296
|
-
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
297
|
-
return value.unknowns.length > 0;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Validate the immutable public `lattice.seam_proposal.v2` artifact.
|
|
302
|
-
*
|
|
303
|
-
* This validates the artifact's closed runtime shape and canonical relations. Binding the
|
|
304
|
-
* recorded conflicts back to the referenced independence bytes is a consumer responsibility.
|
|
305
|
-
*/
|
|
306
|
-
export function validateSeamProposal(value) {
|
|
307
|
-
try {
|
|
308
|
-
if (!exactRecord(value, [
|
|
309
|
-
'schema', 'project_id', 'plan_key', 'source_binding', 'compiled_at',
|
|
310
|
-
'decisions', 'result_digest',
|
|
311
|
-
])
|
|
312
|
-
|| value.schema !== SEAM_PROPOSAL_SCHEMA
|
|
313
|
-
|| !isTodoIdentifier(value.project_id)
|
|
314
|
-
|| !isTodoIdentifier(value.plan_key)
|
|
315
|
-
|| !sourceBinding(value.source_binding)
|
|
316
|
-
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
317
|
-
|| !boundedList(value.decisions, decisionEntry)
|
|
318
|
-
|| !strictlySorted(value.decisions, (decision) => decision.component_id)
|
|
319
|
-
|| !isTodoDigest(value.result_digest)) return false;
|
|
320
|
-
const resourceIds = value.decisions
|
|
321
|
-
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
322
|
-
if (new Set(resourceIds).size !== resourceIds.length) return false;
|
|
323
|
-
return value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
324
|
-
} catch {
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* 案内は、鮮度と載っているunknownから一意に決まる。生成側の文言をそのまま載せる規約なので
|
|
331
|
-
* shapeだけを見ると、投影が状況と噛み合わない案内を載せても通ってしまう。codeは規則正本へ
|
|
332
|
-
* 引き直して照合する(ADR 0130 Decision 1・2)。
|
|
333
|
-
*/
|
|
334
|
-
function projectionGuidance(value, coverage, components) {
|
|
335
|
-
if (!exactRecord(value, ['code', 'message', 'next_action'])
|
|
336
|
-
|| !SEAM_PROPOSAL_GUIDANCE_CODES.includes(value.code)
|
|
337
|
-
|| !boundedText(value.message)
|
|
338
|
-
|| !isTodoIdentifier(value.next_action)) return false;
|
|
339
|
-
const unknownKinds = Array.isArray(components)
|
|
340
|
-
? components.flatMap((component) => (Array.isArray(component?.unknowns)
|
|
341
|
-
? component.unknowns.map((unknown) => unknown?.kind) : []))
|
|
342
|
-
: [];
|
|
343
|
-
return value.code === seamProposalGuidanceCode({ coverage, unknownKinds });
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function projectionComponent(value) {
|
|
347
|
-
if (!exactRecord(value, [
|
|
348
|
-
'component_id', 'verdict', 'task_ids', 'conflicts', 'proposed_surfaces',
|
|
349
|
-
'affected_tests', 'limits', 'reasons', 'unknowns',
|
|
350
|
-
])
|
|
351
|
-
|| !isTodoIdentifier(value.component_id)
|
|
352
|
-
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
353
|
-
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
354
|
-
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
355
|
-
})
|
|
356
|
-
|| !strictlySorted(value.task_ids)
|
|
357
|
-
|| !boundedList(value.reasons, reasonEntry)
|
|
358
|
-
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
359
|
-
|| !boundedList(value.unknowns, unknownEntry)
|
|
360
|
-
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)
|
|
361
|
-
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
362
|
-
|| !strictlySorted(value.affected_tests)
|
|
363
|
-
|| !boundedList(value.limits, isTodoIdentifier)
|
|
364
|
-
|| !strictlySorted(value.limits)) return false;
|
|
365
|
-
const taskIds = new Set(value.task_ids);
|
|
366
|
-
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
367
|
-
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
368
|
-
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)
|
|
369
|
-
|| !boundedList(value.proposed_surfaces,
|
|
370
|
-
(surface) => surfaceEntry(surface, taskIds, { proposed: true }))
|
|
371
|
-
|| !strictlySorted(value.proposed_surfaces, surfaceKey)) return false;
|
|
372
|
-
if (value.verdict === 'seam_candidate') {
|
|
373
|
-
const ownerTaskIds = new Set(value.proposed_surfaces
|
|
374
|
-
.flatMap(({ owner_task_ids: owners }) => owners));
|
|
375
|
-
return value.unknowns.length === 0
|
|
376
|
-
&& value.proposed_surfaces.length > 0
|
|
377
|
-
&& value.limits.includes('structural_only')
|
|
378
|
-
&& ownerTaskIds.size === taskIds.size
|
|
379
|
-
&& [...taskIds].every((taskId) => ownerTaskIds.has(taskId));
|
|
380
|
-
}
|
|
381
|
-
if (value.proposed_surfaces.length > 0
|
|
382
|
-
|| value.affected_tests.length > 0
|
|
383
|
-
|| value.limits.length > 0) return false;
|
|
384
|
-
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
385
|
-
return value.unknowns.length > 0;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Validate the read-only lattice.seam_proposal_projection.v1 public CLI projection.
|
|
390
|
-
* The immutable seam proposal artifact contract above remains independent from this read model.
|
|
391
|
-
*/
|
|
392
|
-
export function validateSeamProposalProjection(value) {
|
|
393
|
-
try {
|
|
394
|
-
if (!exactRecord(value, [
|
|
395
|
-
'schema', 'project_id', 'plan_key', 'coverage', 'compiled_base_sha',
|
|
396
|
-
'current_base_sha', 'plan_version', 'topology_digest', 'independence_result_digest',
|
|
397
|
-
'compiled_at', 'guidance', 'component_count', 'conflict_resource_count',
|
|
398
|
-
'components', 'result_digest',
|
|
399
|
-
])
|
|
400
|
-
|| value.schema !== SEAM_PROPOSAL_PROJECTION_SCHEMA
|
|
401
|
-
|| !isTodoIdentifier(value.project_id)
|
|
402
|
-
|| !isTodoIdentifier(value.plan_key)
|
|
403
|
-
|| !TODO_INDEPENDENCE_COVERAGE.includes(value.coverage)
|
|
404
|
-
|| !isGitSha(value.current_base_sha)
|
|
405
|
-
|| !projectionGuidance(value.guidance, value.coverage, value.components)
|
|
406
|
-
|| !isTodoDigest(value.result_digest)) return false;
|
|
407
|
-
|
|
408
|
-
if (value.coverage === 'missing') {
|
|
409
|
-
return value.compiled_base_sha === null
|
|
410
|
-
&& value.plan_version === null
|
|
411
|
-
&& value.topology_digest === null
|
|
412
|
-
&& value.independence_result_digest === null
|
|
413
|
-
&& value.compiled_at === null
|
|
414
|
-
&& value.component_count === null
|
|
415
|
-
&& value.conflict_resource_count === null
|
|
416
|
-
&& Array.isArray(value.components)
|
|
417
|
-
&& value.components.length === 0
|
|
418
|
-
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (!isGitSha(value.compiled_base_sha)
|
|
422
|
-
|| !isTodoIdentifier(value.plan_version)
|
|
423
|
-
|| !isTodoDigest(value.topology_digest)
|
|
424
|
-
|| !isTodoDigest(value.independence_result_digest)
|
|
425
|
-
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
426
|
-
|| !isNonNegativeSafeInteger(value.component_count)
|
|
427
|
-
|| !isNonNegativeSafeInteger(value.conflict_resource_count)
|
|
428
|
-
|| !boundedList(value.components, projectionComponent)
|
|
429
|
-
|| !strictlySorted(value.components, (component) => component.component_id)
|
|
430
|
-
|| value.component_count !== value.components.length
|
|
431
|
-
|| value.conflict_resource_count !== value.components
|
|
432
|
-
.reduce((count, component) => count + component.conflicts.length, 0)) return false;
|
|
433
|
-
if (value.coverage === 'verified' && value.compiled_base_sha !== value.current_base_sha) {
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
if (value.coverage === 'stale' && value.compiled_base_sha === value.current_base_sha) {
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
439
|
-
const resourceIds = value.components
|
|
440
|
-
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
441
|
-
return new Set(resourceIds).size === resourceIds.length
|
|
442
|
-
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
443
|
-
} catch {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
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 {
|
|
19
|
+
SEAM_PROPOSAL_GUIDANCE_CODES,
|
|
20
|
+
seamProposalGuidanceCode,
|
|
21
|
+
} from './todo-independence-guidance.mjs';
|
|
22
|
+
|
|
23
|
+
export const SEAM_PROPOSAL_SCHEMA = 'lattice.seam_proposal.v2';
|
|
24
|
+
export const SEAM_PROPOSAL_PROJECTION_SCHEMA = 'lattice.seam_proposal_projection.v1';
|
|
25
|
+
export const SEAM_PROPOSAL_VERDICTS = Object.freeze([
|
|
26
|
+
'seam_candidate',
|
|
27
|
+
'intentional_serial',
|
|
28
|
+
'unknown_requires_evidence',
|
|
29
|
+
]);
|
|
30
|
+
export const SEAM_PROPOSAL_STABLE_SURFACE_ROLES = Object.freeze([
|
|
31
|
+
'composition_test',
|
|
32
|
+
'facade',
|
|
33
|
+
'fixed_oracle',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const LIST_LIMIT = 4_096;
|
|
37
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
38
|
+
const PROPOSAL_ID = /^seam-[0-9a-f]{64}$/u;
|
|
39
|
+
const SURFACE_KINDS = Object.freeze(['path', 'symbol']);
|
|
40
|
+
const STABLE_SURFACE_ROLES = new Set(SEAM_PROPOSAL_STABLE_SURFACE_ROLES);
|
|
41
|
+
|
|
42
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
43
|
+
|
|
44
|
+
function boundedText(value, maxBytes = 4_096) {
|
|
45
|
+
return typeof value === 'string' && value.length > 0 && value === value.trim()
|
|
46
|
+
&& !CONTROL.test(value) && Buffer.byteLength(value) <= maxBytes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function boundedList(value, validator, { min = 0, max = LIST_LIMIT } = {}) {
|
|
50
|
+
return Array.isArray(value) && value.length >= min && value.length <= max
|
|
51
|
+
&& value.every(validator);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function strictlySorted(values, key = (value) => value) {
|
|
55
|
+
return values.every((value, index) => index === 0
|
|
56
|
+
|| compareText(key(values[index - 1]), key(value)) < 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function repoRelativePathOrPrefix(value) {
|
|
60
|
+
if (!boundedText(value) || value.startsWith('/') || value.includes('\\')
|
|
61
|
+
|| /^[A-Za-z]:/u.test(value)) return false;
|
|
62
|
+
const body = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
63
|
+
return body.length > 0 && body.split('/')
|
|
64
|
+
.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function repoRelativePath(value) {
|
|
68
|
+
return repoRelativePathOrPrefix(value) && !value.endsWith('/');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sourceBinding(value) {
|
|
72
|
+
return exactRecord(value, [
|
|
73
|
+
'independence_schema', 'independence_result_digest', 'witness_set_digest',
|
|
74
|
+
'plan_version', 'topology_digest', 'base_sha',
|
|
75
|
+
])
|
|
76
|
+
&& value.independence_schema === TODO_INDEPENDENCE_SCHEMA
|
|
77
|
+
&& isTodoDigest(value.independence_result_digest)
|
|
78
|
+
&& isTodoDigest(value.witness_set_digest)
|
|
79
|
+
&& isTodoIdentifier(value.plan_version)
|
|
80
|
+
&& isTodoDigest(value.topology_digest)
|
|
81
|
+
&& isGitSha(value.base_sha);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function taskPair(value, taskIds) {
|
|
85
|
+
return Array.isArray(value) && value.length === 2
|
|
86
|
+
&& value.every(isTodoIdentifier)
|
|
87
|
+
&& compareText(value[0], value[1]) < 0
|
|
88
|
+
&& value.every((taskId) => taskIds.has(taskId));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function conflictEntry(value, taskIds) {
|
|
92
|
+
return exactRecord(value, ['resource_id', 'kind', 'target', 'task_pairs'])
|
|
93
|
+
&& boundedText(value.resource_id)
|
|
94
|
+
&& TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)
|
|
95
|
+
&& (value.kind === 'path'
|
|
96
|
+
? repoRelativePathOrPrefix(value.target)
|
|
97
|
+
: boundedText(value.target))
|
|
98
|
+
&& boundedList(value.task_pairs, (pair) => taskPair(pair, taskIds), { min: 1 })
|
|
99
|
+
&& strictlySorted(value.task_pairs, (pair) => `${pair[0]}\0${pair[1]}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function conflictsFormConnectedComponent(conflicts, taskIds) {
|
|
103
|
+
const adjacency = new Map([...taskIds].map((taskId) => [taskId, new Set()]));
|
|
104
|
+
for (const { task_pairs: pairs } of conflicts) {
|
|
105
|
+
for (const [left, right] of pairs) {
|
|
106
|
+
adjacency.get(left).add(right);
|
|
107
|
+
adjacency.get(right).add(left);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const first = taskIds.values().next().value;
|
|
111
|
+
const visited = new Set([first]);
|
|
112
|
+
const pending = [first];
|
|
113
|
+
while (pending.length > 0) {
|
|
114
|
+
for (const adjacent of adjacency.get(pending.pop())) {
|
|
115
|
+
if (!visited.has(adjacent)) {
|
|
116
|
+
visited.add(adjacent);
|
|
117
|
+
pending.push(adjacent);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return visited.size === taskIds.size;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const surfaceKey = (value) => `${value.kind}\0${value.target}\0${value.path}\0${value.role}`;
|
|
125
|
+
|
|
126
|
+
function surfaceEntry(value, taskIds, { proposed }) {
|
|
127
|
+
if (!exactRecord(value, ['kind', 'target', 'path', 'role', 'owner_task_ids'])
|
|
128
|
+
|| !SURFACE_KINDS.includes(value.kind)
|
|
129
|
+
|| !repoRelativePathOrPrefix(value.path)
|
|
130
|
+
|| !boundedText(value.target)
|
|
131
|
+
|| !isTodoIdentifier(value.role)
|
|
132
|
+
|| !boundedList(value.owner_task_ids, isTodoIdentifier, {
|
|
133
|
+
max: proposed ? 1 : TODO_INDEPENDENCE_TASK_LIMIT,
|
|
134
|
+
})
|
|
135
|
+
|| !strictlySorted(value.owner_task_ids)
|
|
136
|
+
|| !value.owner_task_ids.every((taskId) => taskIds.has(taskId))) return false;
|
|
137
|
+
if (value.kind === 'path' && value.target !== value.path) return false;
|
|
138
|
+
if (value.kind === 'symbol' && !repoRelativePath(value.path)) return false;
|
|
139
|
+
if (!proposed) return true;
|
|
140
|
+
return STABLE_SURFACE_ROLES.has(value.role)
|
|
141
|
+
? value.owner_task_ids.length === 0
|
|
142
|
+
: value.owner_task_ids.length === 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `candidate_paths`は、同名symbolが複数fileに居た時だけ埋まる(ADR 0134)。
|
|
147
|
+
*
|
|
148
|
+
* 単数の`resolved_path`と併存させるのは、両者が別の事実だからである。前者は「一意に決まった」、
|
|
149
|
+
* 後者は「決まらず、候補はこれだけあった」。候補を持つ受け皿が無かった頃、後者は`unknown`へ
|
|
150
|
+
* 潰れ、宣言の`within`で絞れる情報が記録に残らなかった。一意に決まった時に候補を並べない
|
|
151
|
+
* (空配列に限る)のは、決まった事実と決まらなかった事実を同じ形にしないためである。
|
|
152
|
+
*/
|
|
153
|
+
function queryEntry(value) {
|
|
154
|
+
return exactRecord(value, [
|
|
155
|
+
'query_id', 'operation', 'target', 'outcome', 'resolved_name', 'resolved_path',
|
|
156
|
+
'candidate_paths', 'result_digest',
|
|
157
|
+
])
|
|
158
|
+
&& isTodoIdentifier(value.query_id)
|
|
159
|
+
&& isTodoIdentifier(value.operation)
|
|
160
|
+
&& boundedText(value.target)
|
|
161
|
+
&& isTodoIdentifier(value.outcome)
|
|
162
|
+
&& (value.resolved_name === null || boundedText(value.resolved_name))
|
|
163
|
+
&& (value.resolved_path === null || repoRelativePath(value.resolved_path))
|
|
164
|
+
&& boundedList(value.candidate_paths, repoRelativePath)
|
|
165
|
+
&& strictlySorted(value.candidate_paths)
|
|
166
|
+
&& (value.outcome === 'ambiguous'
|
|
167
|
+
? value.candidate_paths.length >= 2 && value.resolved_path === null
|
|
168
|
+
&& boundedText(value.resolved_name)
|
|
169
|
+
: value.candidate_paths.length === 0)
|
|
170
|
+
&& isTodoDigest(value.result_digest);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* `query_set_digest` is an external binding to the exact sensor query set input used for
|
|
175
|
+
* collection (the same meaning as `digestArtifact(request.sensor_query_set)` in the runtime
|
|
176
|
+
* front end). That input is not embedded here, so this validator can check only digest shape.
|
|
177
|
+
* `evidence_digest` separately self-seals this record, including that binding and every receipt.
|
|
178
|
+
*/
|
|
179
|
+
function evidenceEntry(value) {
|
|
180
|
+
return exactRecord(value, ['query_set_digest', 'evidence_digest', 'queries'])
|
|
181
|
+
&& isTodoDigest(value.query_set_digest)
|
|
182
|
+
&& isTodoDigest(value.evidence_digest)
|
|
183
|
+
&& boundedList(value.queries, queryEntry)
|
|
184
|
+
&& strictlySorted(value.queries, (query) => query.query_id)
|
|
185
|
+
&& value.evidence_digest === todoSelfDigest(value, 'evidence_digest');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function verificationEntry(value) {
|
|
189
|
+
return exactRecord(value, [
|
|
190
|
+
'virtual_compile_input_digest', 'virtual_compile_result_digest', 'residual_conflicts',
|
|
191
|
+
])
|
|
192
|
+
&& isTodoDigest(value.virtual_compile_input_digest)
|
|
193
|
+
&& isTodoDigest(value.virtual_compile_result_digest)
|
|
194
|
+
&& Array.isArray(value.residual_conflicts)
|
|
195
|
+
&& value.residual_conflicts.length === 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeConflictIdentity(conflicts) {
|
|
199
|
+
return conflicts.map(({ resource_id: resourceId, kind, target, task_pairs: taskPairs }) => ({
|
|
200
|
+
resource_id: resourceId,
|
|
201
|
+
kind,
|
|
202
|
+
target,
|
|
203
|
+
task_pairs: taskPairs.map(([left, right]) => [left, right])
|
|
204
|
+
.sort((left, right) => compareText(`${left[0]}\0${left[1]}`, `${right[0]}\0${right[1]}`)),
|
|
205
|
+
})).sort((left, right) => compareText(left.resource_id, right.resource_id));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeProposedOwnership(surfaces) {
|
|
209
|
+
return surfaces.map(({ kind, target, path, role, owner_task_ids: ownerTaskIds }) => ({
|
|
210
|
+
kind,
|
|
211
|
+
target,
|
|
212
|
+
path,
|
|
213
|
+
role,
|
|
214
|
+
owner_task_ids: [...ownerTaskIds].sort(compareText),
|
|
215
|
+
})).sort((left, right) => compareText(surfaceKey(left), surfaceKey(right)));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Content receipts and source freshness deliberately do not participate in semantic identity.
|
|
220
|
+
* The caller still has to pass the same conflict component and proposed ownership shape.
|
|
221
|
+
*/
|
|
222
|
+
export function deriveSeamProposalId({ conflicts, proposed_surfaces: proposedSurfaces }) {
|
|
223
|
+
const semanticKey = {
|
|
224
|
+
conflicts: normalizeConflictIdentity(conflicts),
|
|
225
|
+
proposed_surfaces: normalizeProposedOwnership(proposedSurfaces),
|
|
226
|
+
};
|
|
227
|
+
return `seam-${digestTodoArtifact(semanticKey)}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function seamCandidate(value, conflicts, taskIds) {
|
|
231
|
+
if (!exactRecord(value, [
|
|
232
|
+
'proposal_id', 'current_surfaces', 'proposed_surfaces', 'affected_tests',
|
|
233
|
+
'verification', 'evidence', 'limits', 'proposal_digest',
|
|
234
|
+
])
|
|
235
|
+
|| !PROPOSAL_ID.test(value.proposal_id)
|
|
236
|
+
|| !boundedList(value.current_surfaces,
|
|
237
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: false }))
|
|
238
|
+
|| !strictlySorted(value.current_surfaces, surfaceKey)
|
|
239
|
+
|| !boundedList(value.proposed_surfaces,
|
|
240
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: true }), { min: 1 })
|
|
241
|
+
|| !strictlySorted(value.proposed_surfaces, surfaceKey)
|
|
242
|
+
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
243
|
+
|| !strictlySorted(value.affected_tests)
|
|
244
|
+
|| !verificationEntry(value.verification)
|
|
245
|
+
|| !evidenceEntry(value.evidence)
|
|
246
|
+
|| !boundedList(value.limits, isTodoIdentifier, { min: 1 })
|
|
247
|
+
|| !strictlySorted(value.limits)
|
|
248
|
+
|| !value.limits.includes('structural_only')
|
|
249
|
+
|| !isTodoDigest(value.proposal_digest)) return false;
|
|
250
|
+
const proposedOwnerTaskIds = new Set(value.proposed_surfaces
|
|
251
|
+
.flatMap(({ owner_task_ids: ownerTaskIds }) => ownerTaskIds));
|
|
252
|
+
if (proposedOwnerTaskIds.size !== taskIds.size
|
|
253
|
+
|| ![...taskIds].every((taskId) => proposedOwnerTaskIds.has(taskId))) return false;
|
|
254
|
+
if (value.proposal_id !== deriveSeamProposalId({
|
|
255
|
+
conflicts,
|
|
256
|
+
proposed_surfaces: value.proposed_surfaces,
|
|
257
|
+
})) return false;
|
|
258
|
+
return value.proposal_digest === todoSelfDigest(value, 'proposal_digest');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function reasonEntry(value) {
|
|
262
|
+
return exactRecord(value, ['code', 'detail'])
|
|
263
|
+
&& isTodoIdentifier(value.code) && boundedText(value.detail);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function unknownEntry(value) {
|
|
267
|
+
return exactRecord(value, ['kind', 'ref'])
|
|
268
|
+
&& isTodoIdentifier(value.kind) && boundedText(value.ref);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function decisionEntry(value) {
|
|
272
|
+
if (!exactRecord(value, [
|
|
273
|
+
'component_id', 'task_ids', 'conflicts', 'verdict', 'seam_candidate',
|
|
274
|
+
'reasons', 'unknowns',
|
|
275
|
+
])
|
|
276
|
+
|| !isTodoIdentifier(value.component_id)
|
|
277
|
+
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
278
|
+
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
279
|
+
})
|
|
280
|
+
|| !strictlySorted(value.task_ids)
|
|
281
|
+
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
282
|
+
|| !boundedList(value.reasons, reasonEntry)
|
|
283
|
+
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
284
|
+
|| !boundedList(value.unknowns, unknownEntry)
|
|
285
|
+
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)) return false;
|
|
286
|
+
const taskIds = new Set(value.task_ids);
|
|
287
|
+
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
288
|
+
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
289
|
+
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)) return false;
|
|
290
|
+
if (value.verdict === 'seam_candidate') {
|
|
291
|
+
return value.unknowns.length === 0
|
|
292
|
+
&& value.conflicts.every(({ kind }) => severabilityOfConflictKind(kind) === 'code_seam')
|
|
293
|
+
&& seamCandidate(value.seam_candidate, value.conflicts, taskIds);
|
|
294
|
+
}
|
|
295
|
+
if (value.seam_candidate !== null) return false;
|
|
296
|
+
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
297
|
+
return value.unknowns.length > 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Validate the immutable public `lattice.seam_proposal.v2` artifact.
|
|
302
|
+
*
|
|
303
|
+
* This validates the artifact's closed runtime shape and canonical relations. Binding the
|
|
304
|
+
* recorded conflicts back to the referenced independence bytes is a consumer responsibility.
|
|
305
|
+
*/
|
|
306
|
+
export function validateSeamProposal(value) {
|
|
307
|
+
try {
|
|
308
|
+
if (!exactRecord(value, [
|
|
309
|
+
'schema', 'project_id', 'plan_key', 'source_binding', 'compiled_at',
|
|
310
|
+
'decisions', 'result_digest',
|
|
311
|
+
])
|
|
312
|
+
|| value.schema !== SEAM_PROPOSAL_SCHEMA
|
|
313
|
+
|| !isTodoIdentifier(value.project_id)
|
|
314
|
+
|| !isTodoIdentifier(value.plan_key)
|
|
315
|
+
|| !sourceBinding(value.source_binding)
|
|
316
|
+
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
317
|
+
|| !boundedList(value.decisions, decisionEntry)
|
|
318
|
+
|| !strictlySorted(value.decisions, (decision) => decision.component_id)
|
|
319
|
+
|| !isTodoDigest(value.result_digest)) return false;
|
|
320
|
+
const resourceIds = value.decisions
|
|
321
|
+
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
322
|
+
if (new Set(resourceIds).size !== resourceIds.length) return false;
|
|
323
|
+
return value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 案内は、鮮度と載っているunknownから一意に決まる。生成側の文言をそのまま載せる規約なので
|
|
331
|
+
* shapeだけを見ると、投影が状況と噛み合わない案内を載せても通ってしまう。codeは規則正本へ
|
|
332
|
+
* 引き直して照合する(ADR 0130 Decision 1・2)。
|
|
333
|
+
*/
|
|
334
|
+
function projectionGuidance(value, coverage, components) {
|
|
335
|
+
if (!exactRecord(value, ['code', 'message', 'next_action'])
|
|
336
|
+
|| !SEAM_PROPOSAL_GUIDANCE_CODES.includes(value.code)
|
|
337
|
+
|| !boundedText(value.message)
|
|
338
|
+
|| !isTodoIdentifier(value.next_action)) return false;
|
|
339
|
+
const unknownKinds = Array.isArray(components)
|
|
340
|
+
? components.flatMap((component) => (Array.isArray(component?.unknowns)
|
|
341
|
+
? component.unknowns.map((unknown) => unknown?.kind) : []))
|
|
342
|
+
: [];
|
|
343
|
+
return value.code === seamProposalGuidanceCode({ coverage, unknownKinds });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function projectionComponent(value) {
|
|
347
|
+
if (!exactRecord(value, [
|
|
348
|
+
'component_id', 'verdict', 'task_ids', 'conflicts', 'proposed_surfaces',
|
|
349
|
+
'affected_tests', 'limits', 'reasons', 'unknowns',
|
|
350
|
+
])
|
|
351
|
+
|| !isTodoIdentifier(value.component_id)
|
|
352
|
+
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
353
|
+
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
354
|
+
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
355
|
+
})
|
|
356
|
+
|| !strictlySorted(value.task_ids)
|
|
357
|
+
|| !boundedList(value.reasons, reasonEntry)
|
|
358
|
+
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
359
|
+
|| !boundedList(value.unknowns, unknownEntry)
|
|
360
|
+
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)
|
|
361
|
+
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
362
|
+
|| !strictlySorted(value.affected_tests)
|
|
363
|
+
|| !boundedList(value.limits, isTodoIdentifier)
|
|
364
|
+
|| !strictlySorted(value.limits)) return false;
|
|
365
|
+
const taskIds = new Set(value.task_ids);
|
|
366
|
+
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
367
|
+
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
368
|
+
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)
|
|
369
|
+
|| !boundedList(value.proposed_surfaces,
|
|
370
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: true }))
|
|
371
|
+
|| !strictlySorted(value.proposed_surfaces, surfaceKey)) return false;
|
|
372
|
+
if (value.verdict === 'seam_candidate') {
|
|
373
|
+
const ownerTaskIds = new Set(value.proposed_surfaces
|
|
374
|
+
.flatMap(({ owner_task_ids: owners }) => owners));
|
|
375
|
+
return value.unknowns.length === 0
|
|
376
|
+
&& value.proposed_surfaces.length > 0
|
|
377
|
+
&& value.limits.includes('structural_only')
|
|
378
|
+
&& ownerTaskIds.size === taskIds.size
|
|
379
|
+
&& [...taskIds].every((taskId) => ownerTaskIds.has(taskId));
|
|
380
|
+
}
|
|
381
|
+
if (value.proposed_surfaces.length > 0
|
|
382
|
+
|| value.affected_tests.length > 0
|
|
383
|
+
|| value.limits.length > 0) return false;
|
|
384
|
+
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
385
|
+
return value.unknowns.length > 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Validate the read-only lattice.seam_proposal_projection.v1 public CLI projection.
|
|
390
|
+
* The immutable seam proposal artifact contract above remains independent from this read model.
|
|
391
|
+
*/
|
|
392
|
+
export function validateSeamProposalProjection(value) {
|
|
393
|
+
try {
|
|
394
|
+
if (!exactRecord(value, [
|
|
395
|
+
'schema', 'project_id', 'plan_key', 'coverage', 'compiled_base_sha',
|
|
396
|
+
'current_base_sha', 'plan_version', 'topology_digest', 'independence_result_digest',
|
|
397
|
+
'compiled_at', 'guidance', 'component_count', 'conflict_resource_count',
|
|
398
|
+
'components', 'result_digest',
|
|
399
|
+
])
|
|
400
|
+
|| value.schema !== SEAM_PROPOSAL_PROJECTION_SCHEMA
|
|
401
|
+
|| !isTodoIdentifier(value.project_id)
|
|
402
|
+
|| !isTodoIdentifier(value.plan_key)
|
|
403
|
+
|| !TODO_INDEPENDENCE_COVERAGE.includes(value.coverage)
|
|
404
|
+
|| !isGitSha(value.current_base_sha)
|
|
405
|
+
|| !projectionGuidance(value.guidance, value.coverage, value.components)
|
|
406
|
+
|| !isTodoDigest(value.result_digest)) return false;
|
|
407
|
+
|
|
408
|
+
if (value.coverage === 'missing') {
|
|
409
|
+
return value.compiled_base_sha === null
|
|
410
|
+
&& value.plan_version === null
|
|
411
|
+
&& value.topology_digest === null
|
|
412
|
+
&& value.independence_result_digest === null
|
|
413
|
+
&& value.compiled_at === null
|
|
414
|
+
&& value.component_count === null
|
|
415
|
+
&& value.conflict_resource_count === null
|
|
416
|
+
&& Array.isArray(value.components)
|
|
417
|
+
&& value.components.length === 0
|
|
418
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!isGitSha(value.compiled_base_sha)
|
|
422
|
+
|| !isTodoIdentifier(value.plan_version)
|
|
423
|
+
|| !isTodoDigest(value.topology_digest)
|
|
424
|
+
|| !isTodoDigest(value.independence_result_digest)
|
|
425
|
+
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
426
|
+
|| !isNonNegativeSafeInteger(value.component_count)
|
|
427
|
+
|| !isNonNegativeSafeInteger(value.conflict_resource_count)
|
|
428
|
+
|| !boundedList(value.components, projectionComponent)
|
|
429
|
+
|| !strictlySorted(value.components, (component) => component.component_id)
|
|
430
|
+
|| value.component_count !== value.components.length
|
|
431
|
+
|| value.conflict_resource_count !== value.components
|
|
432
|
+
.reduce((count, component) => count + component.conflicts.length, 0)) return false;
|
|
433
|
+
if (value.coverage === 'verified' && value.compiled_base_sha !== value.current_base_sha) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
if (value.coverage === 'stale' && value.compiled_base_sha === value.current_base_sha) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const resourceIds = value.components
|
|
440
|
+
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
441
|
+
return new Set(resourceIds).size === resourceIds.length
|
|
442
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
443
|
+
} catch {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|