@quolu/lattice 0.52.0 → 0.52.2
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/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- 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-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/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1053
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +697 -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 -1502
- 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 -520
- package/src/runtime-cli.mjs +4588 -4584
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -600
- 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 -162
- 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 -477
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -290
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -834
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1188
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
- 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 -1167
- 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 +3141 -3126
- 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 -625
- 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 +4322 -4314
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,594 +1,594 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
canonicalizeArtifact,
|
|
6
|
-
digestArtifact,
|
|
7
|
-
validateTransformArtifact,
|
|
8
|
-
} from './artifact-contracts.mjs';
|
|
9
|
-
import { evaluateRc1Hypothesis } from './rc1-comparison.mjs';
|
|
10
|
-
|
|
11
|
-
const SHA256 = /^[0-9a-f]{64}$/;
|
|
12
|
-
const GIT_SHA1 = /^[0-9a-f]{40}$/;
|
|
13
|
-
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
14
|
-
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
15
|
-
const REQUIRED_BEHAVIOR_PATHS = Object.freeze([
|
|
16
|
-
'behavior/evidence-envelope.json',
|
|
17
|
-
'behavior/post-receipt.json',
|
|
18
|
-
'behavior/pre-receipt.json',
|
|
19
|
-
'transform/seam.patch',
|
|
20
|
-
'transform/transform-artifact.json',
|
|
21
|
-
].sort());
|
|
22
|
-
const RECEIPT_KEYS = Object.freeze([
|
|
23
|
-
'schema',
|
|
24
|
-
'role',
|
|
25
|
-
'base_sha',
|
|
26
|
-
'oracle_digest',
|
|
27
|
-
'entrypoint',
|
|
28
|
-
'export_name',
|
|
29
|
-
'entrypoint_content_digest',
|
|
30
|
-
'surface',
|
|
31
|
-
'surface_digest',
|
|
32
|
-
'observation',
|
|
33
|
-
'outcome',
|
|
34
|
-
'case_results',
|
|
35
|
-
'receipt_digest',
|
|
36
|
-
]);
|
|
37
|
-
const ENVELOPE_KEYS = Object.freeze([
|
|
38
|
-
'schema',
|
|
39
|
-
'base_sha',
|
|
40
|
-
'oracle_digest',
|
|
41
|
-
'pre_receipt_digest',
|
|
42
|
-
'post_receipt_digest',
|
|
43
|
-
'pre_surface_digest',
|
|
44
|
-
'post_surface_digest',
|
|
45
|
-
'transform_artifact_digest',
|
|
46
|
-
'patch_digest',
|
|
47
|
-
'output_snapshot_digest',
|
|
48
|
-
'envelope_digest',
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
function isPlainObject(value) {
|
|
52
|
-
return value !== null
|
|
53
|
-
&& typeof value === 'object'
|
|
54
|
-
&& !Array.isArray(value)
|
|
55
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function exactRecord(value, keys) {
|
|
59
|
-
if (!isPlainObject(value)) return false;
|
|
60
|
-
const actual = Object.keys(value).sort();
|
|
61
|
-
const expected = [...keys].sort();
|
|
62
|
-
return actual.length === expected.length
|
|
63
|
-
&& actual.every((key, index) => key === expected[index]);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function repoPath(value) {
|
|
67
|
-
return typeof value === 'string'
|
|
68
|
-
&& value.length > 0
|
|
69
|
-
&& value === value.trim()
|
|
70
|
-
&& Buffer.byteLength(value, 'utf8') <= 1_024
|
|
71
|
-
&& !CONTROL_CHARACTER.test(value)
|
|
72
|
-
&& !path.posix.isAbsolute(value)
|
|
73
|
-
&& !value.startsWith('/')
|
|
74
|
-
&& !value.endsWith('/')
|
|
75
|
-
&& !value.includes('\\')
|
|
76
|
-
&& !/^[A-Za-z]:/.test(value)
|
|
77
|
-
&& value.split('/').every((segment) => (
|
|
78
|
-
segment !== '' && segment !== '.' && segment !== '..'
|
|
79
|
-
));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function digest(value) {
|
|
83
|
-
return typeof value === 'string' && SHA256.test(value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function sameArray(left, right) {
|
|
87
|
-
return Array.isArray(left)
|
|
88
|
-
&& Array.isArray(right)
|
|
89
|
-
&& left.length === right.length
|
|
90
|
-
&& left.every((entry, index) => entry === right[index]);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function uniqueSortedStrings(values) {
|
|
94
|
-
if (!Array.isArray(values) || values.length === 0) return false;
|
|
95
|
-
const sorted = [...values].sort();
|
|
96
|
-
return values.every((value, index) => (
|
|
97
|
-
typeof value === 'string'
|
|
98
|
-
&& value === sorted[index]
|
|
99
|
-
&& (index === 0 || value !== values[index - 1])
|
|
100
|
-
));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function artifactDigestWithout(value, digestKey) {
|
|
104
|
-
const { [digestKey]: ignored, ...preimage } = value;
|
|
105
|
-
return digestArtifact(preimage);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function validateSurface(value) {
|
|
109
|
-
try {
|
|
110
|
-
if (!exactRecord(value, ['schema', 'files'])
|
|
111
|
-
|| value.schema !== 'lattice.rc1.behavior_surface_snapshot.v1'
|
|
112
|
-
|| !Array.isArray(value.files)
|
|
113
|
-
|| value.files.length === 0
|
|
114
|
-
|| value.files.length > 128) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
const paths = value.files.map(({ path: filePath }) => filePath);
|
|
118
|
-
if (!uniqueSortedStrings(paths)) return false;
|
|
119
|
-
for (const file of value.files) {
|
|
120
|
-
if (!exactRecord(file, ['path', 'state', 'content_digest'])
|
|
121
|
-
|| !repoPath(file.path)
|
|
122
|
-
|| (file.state !== 'present' && file.state !== 'absent')
|
|
123
|
-
|| (file.state === 'present' && !digest(file.content_digest))
|
|
124
|
-
|| (file.state === 'absent' && file.content_digest !== null)) {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
canonicalizeArtifact(value);
|
|
129
|
-
return true;
|
|
130
|
-
} catch {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function validateCaseResult(value) {
|
|
136
|
-
return exactRecord(value, [
|
|
137
|
-
'id',
|
|
138
|
-
'outcome',
|
|
139
|
-
'observed_kind',
|
|
140
|
-
'expected_digest',
|
|
141
|
-
'observed_digest',
|
|
142
|
-
])
|
|
143
|
-
&& typeof value.id === 'string'
|
|
144
|
-
&& IDENTIFIER.test(value.id)
|
|
145
|
-
&& (value.outcome === 'passed' || value.outcome === 'failed')
|
|
146
|
-
&& (value.observed_kind === 'return' || value.observed_kind === 'throw')
|
|
147
|
-
&& digest(value.expected_digest)
|
|
148
|
-
&& digest(value.observed_digest);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function validateReceipt(value) {
|
|
152
|
-
if (!exactRecord(value, RECEIPT_KEYS)
|
|
153
|
-
|| value.schema !== 'lattice.rc1.black_box_behavior_receipt.v3'
|
|
154
|
-
|| (value.role !== 'pre' && value.role !== 'post')
|
|
155
|
-
|| !GIT_SHA1.test(value.base_sha)
|
|
156
|
-
|| !digest(value.oracle_digest)
|
|
157
|
-
|| !repoPath(value.entrypoint)
|
|
158
|
-
|| typeof value.export_name !== 'string'
|
|
159
|
-
|| !IDENTIFIER.test(value.export_name)
|
|
160
|
-
|| !digest(value.entrypoint_content_digest)
|
|
161
|
-
|| !validateSurface(value.surface)
|
|
162
|
-
|| !digest(value.surface_digest)
|
|
163
|
-
|| value.surface_digest !== digestArtifact(value.surface)
|
|
164
|
-
|| !exactRecord(value.observation, [
|
|
165
|
-
'before_surface_digest',
|
|
166
|
-
'after_surface_digest',
|
|
167
|
-
])
|
|
168
|
-
|| value.observation.before_surface_digest !== value.surface_digest
|
|
169
|
-
|| value.observation.after_surface_digest !== value.surface_digest
|
|
170
|
-
|| (value.outcome !== 'passed' && value.outcome !== 'failed')
|
|
171
|
-
|| !Array.isArray(value.case_results)
|
|
172
|
-
|| value.case_results.length === 0
|
|
173
|
-
|| value.case_results.length > 64
|
|
174
|
-
|| !value.case_results.every(validateCaseResult)
|
|
175
|
-
|| new Set(value.case_results.map(({ id }) => id)).size !== value.case_results.length
|
|
176
|
-
|| (value.outcome === 'passed') !== value.case_results.every(({ outcome }) => outcome === 'passed')
|
|
177
|
-
|| !digest(value.receipt_digest)
|
|
178
|
-
|| value.receipt_digest !== artifactDigestWithout(value, 'receipt_digest')) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
const entrypoint = value.surface.files.find(({ path: filePath }) => filePath === value.entrypoint);
|
|
182
|
-
return entrypoint?.state === 'present'
|
|
183
|
-
&& entrypoint.content_digest === value.entrypoint_content_digest;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** @param {unknown} value @returns {boolean} */
|
|
187
|
-
export function validateRc1V5BehaviorSurface(value) {
|
|
188
|
-
return validateSurface(value);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** @param {unknown} value @returns {boolean} */
|
|
192
|
-
export function validateRc1V5BehaviorReceipt(value) {
|
|
193
|
-
return validateReceipt(value);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function validateEnvelope(value) {
|
|
197
|
-
return exactRecord(value, ENVELOPE_KEYS)
|
|
198
|
-
&& value.schema === 'lattice.rc1.behavior_evidence_envelope.v1'
|
|
199
|
-
&& GIT_SHA1.test(value.base_sha)
|
|
200
|
-
&& [
|
|
201
|
-
value.oracle_digest,
|
|
202
|
-
value.pre_receipt_digest,
|
|
203
|
-
value.post_receipt_digest,
|
|
204
|
-
value.pre_surface_digest,
|
|
205
|
-
value.post_surface_digest,
|
|
206
|
-
value.transform_artifact_digest,
|
|
207
|
-
value.patch_digest,
|
|
208
|
-
value.output_snapshot_digest,
|
|
209
|
-
value.envelope_digest,
|
|
210
|
-
].every(digest)
|
|
211
|
-
&& value.envelope_digest === artifactDigestWithout(value, 'envelope_digest');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function surfacePaths(surface) {
|
|
215
|
-
return validateSurface(surface) ? surface.files.map(({ path: filePath }) => filePath) : [];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function outputProjection(surface) {
|
|
219
|
-
if (!validateSurface(surface) || surface.files.some(({ state }) => state !== 'present')) {
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
files: surface.files.map(({ path: filePath, content_digest: contentDigest }) => ({
|
|
224
|
-
path: filePath,
|
|
225
|
-
content_digest: contentDigest,
|
|
226
|
-
})),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function validAcceptedTransform(value) {
|
|
231
|
-
if (!validateTransformArtifact(value)
|
|
232
|
-
|| value.status !== 'accepted'
|
|
233
|
-
|| !GIT_SHA1.test(value.source?.base_sha)
|
|
234
|
-
|| !digest(value.patch?.digest)
|
|
235
|
-
|| !digest(value.output?.snapshot_digest)
|
|
236
|
-
|| !Array.isArray(value.output?.files)
|
|
237
|
-
|| value.output.files.length === 0) {
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
const paths = value.output.files.map(({ path: filePath }) => filePath);
|
|
241
|
-
return uniqueSortedStrings(paths)
|
|
242
|
-
&& value.output.files.every((file) => (
|
|
243
|
-
exactRecord(file, ['path', 'content_digest'])
|
|
244
|
-
&& repoPath(file.path)
|
|
245
|
-
&& digest(file.content_digest)
|
|
246
|
-
))
|
|
247
|
-
&& value.output.snapshot_digest === digestArtifact({ files: value.output.files });
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function behaviorBindingChecks(value) {
|
|
251
|
-
const validInput = exactRecord(value, [
|
|
252
|
-
'pre_receipt',
|
|
253
|
-
'post_receipt',
|
|
254
|
-
'envelope',
|
|
255
|
-
'transform_artifact',
|
|
256
|
-
'patch_digest',
|
|
257
|
-
]);
|
|
258
|
-
const pre = validInput ? value.pre_receipt : null;
|
|
259
|
-
const post = validInput ? value.post_receipt : null;
|
|
260
|
-
const envelope = validInput ? value.envelope : null;
|
|
261
|
-
const transform = validInput ? value.transform_artifact : null;
|
|
262
|
-
const patchDigest = validInput ? value.patch_digest : null;
|
|
263
|
-
const preValid = validateReceipt(pre);
|
|
264
|
-
const postValid = validateReceipt(post);
|
|
265
|
-
const envelopeValid = validateEnvelope(envelope);
|
|
266
|
-
const transformValid = validAcceptedTransform(transform);
|
|
267
|
-
const prePaths = preValid ? surfacePaths(pre.surface) : [];
|
|
268
|
-
const postPaths = postValid ? surfacePaths(post.surface) : [];
|
|
269
|
-
const projection = postValid ? outputProjection(post.surface) : null;
|
|
270
|
-
const preSourceBound = preValid
|
|
271
|
-
&& transformValid
|
|
272
|
-
&& pre.surface_digest === transform.source.code_snapshot_digest;
|
|
273
|
-
const outputBound = transformValid
|
|
274
|
-
&& projection !== null
|
|
275
|
-
&& sameArray(prePaths, postPaths)
|
|
276
|
-
&& sameArray(postPaths, transform.scope.allowed_paths)
|
|
277
|
-
&& digestArtifact(projection) === transform.output.snapshot_digest
|
|
278
|
-
&& digestArtifact(projection.files) === digestArtifact(transform.output.files);
|
|
279
|
-
|
|
280
|
-
return [
|
|
281
|
-
{ id: 'receipt_schema', passed: preValid && postValid },
|
|
282
|
-
{
|
|
283
|
-
id: 'receipt_identity',
|
|
284
|
-
passed: preValid && postValid
|
|
285
|
-
&& pre.role === 'pre'
|
|
286
|
-
&& post.role === 'post'
|
|
287
|
-
&& pre.receipt_digest !== post.receipt_digest,
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
id: 'base_binding',
|
|
291
|
-
passed: preValid && postValid && transformValid
|
|
292
|
-
&& pre.base_sha === post.base_sha
|
|
293
|
-
&& pre.base_sha === transform.source.base_sha,
|
|
294
|
-
},
|
|
295
|
-
{ id: 'pre_source_binding', passed: preSourceBound },
|
|
296
|
-
{
|
|
297
|
-
id: 'oracle_binding',
|
|
298
|
-
passed: preValid && postValid
|
|
299
|
-
&& pre.oracle_digest === post.oracle_digest
|
|
300
|
-
&& pre.entrypoint === post.entrypoint
|
|
301
|
-
&& pre.export_name === post.export_name,
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
id: 'behavior_outcome',
|
|
305
|
-
passed: preValid && postValid && pre.outcome === 'passed' && post.outcome === 'passed',
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
id: 'surface_binding',
|
|
309
|
-
passed: preValid && postValid && sameArray(prePaths, postPaths),
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
id: 'transform_binding',
|
|
313
|
-
passed: transformValid && envelopeValid
|
|
314
|
-
&& envelope.transform_artifact_digest === digestArtifact(transform),
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
id: 'patch_binding',
|
|
318
|
-
passed: transformValid && envelopeValid && digest(patchDigest)
|
|
319
|
-
&& patchDigest === transform.patch.digest
|
|
320
|
-
&& patchDigest === envelope.patch_digest,
|
|
321
|
-
},
|
|
322
|
-
{ id: 'post_output_binding', passed: outputBound },
|
|
323
|
-
{
|
|
324
|
-
id: 'envelope_binding',
|
|
325
|
-
passed: preValid && postValid && transformValid && envelopeValid && outputBound
|
|
326
|
-
&& envelope.base_sha === pre.base_sha
|
|
327
|
-
&& envelope.oracle_digest === pre.oracle_digest
|
|
328
|
-
&& envelope.pre_receipt_digest === pre.receipt_digest
|
|
329
|
-
&& envelope.post_receipt_digest === post.receipt_digest
|
|
330
|
-
&& envelope.pre_surface_digest === pre.surface_digest
|
|
331
|
-
&& envelope.post_surface_digest === post.surface_digest
|
|
332
|
-
&& envelope.output_snapshot_digest === transform.output.snapshot_digest,
|
|
333
|
-
},
|
|
334
|
-
];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function fail(reason) {
|
|
338
|
-
throw new TypeError(`RC1 v5 behavior evidence契約違反: ${reason}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* full pre/post receiptをaccepted transformとpatchへcross-bindする。
|
|
343
|
-
*/
|
|
344
|
-
export function compileRc1V5BehaviorEvidence(options = {}) {
|
|
345
|
-
if (!exactRecord(options, [
|
|
346
|
-
'preReceipt',
|
|
347
|
-
'postReceipt',
|
|
348
|
-
'transformArtifact',
|
|
349
|
-
'patchDigest',
|
|
350
|
-
])) {
|
|
351
|
-
fail('input shapeが不正');
|
|
352
|
-
}
|
|
353
|
-
const {
|
|
354
|
-
preReceipt,
|
|
355
|
-
postReceipt,
|
|
356
|
-
transformArtifact,
|
|
357
|
-
patchDigest,
|
|
358
|
-
} = options;
|
|
359
|
-
if (!validateReceipt(preReceipt) || !validateReceipt(postReceipt)) fail('full receiptが不正');
|
|
360
|
-
if (!validAcceptedTransform(transformArtifact)) fail('accepted transform artifactが不正');
|
|
361
|
-
if (!digest(patchDigest) || patchDigest !== transformArtifact.patch.digest) {
|
|
362
|
-
fail('patch digestがaccepted transformと不一致');
|
|
363
|
-
}
|
|
364
|
-
const preimage = {
|
|
365
|
-
schema: 'lattice.rc1.behavior_evidence_envelope.v1',
|
|
366
|
-
base_sha: preReceipt.base_sha,
|
|
367
|
-
oracle_digest: preReceipt.oracle_digest,
|
|
368
|
-
pre_receipt_digest: preReceipt.receipt_digest,
|
|
369
|
-
post_receipt_digest: postReceipt.receipt_digest,
|
|
370
|
-
pre_surface_digest: preReceipt.surface_digest,
|
|
371
|
-
post_surface_digest: postReceipt.surface_digest,
|
|
372
|
-
transform_artifact_digest: digestArtifact(transformArtifact),
|
|
373
|
-
patch_digest: patchDigest,
|
|
374
|
-
output_snapshot_digest: transformArtifact.output.snapshot_digest,
|
|
375
|
-
};
|
|
376
|
-
const envelope = { ...preimage, envelope_digest: digestArtifact(preimage) };
|
|
377
|
-
const checks = behaviorBindingChecks({
|
|
378
|
-
pre_receipt: preReceipt,
|
|
379
|
-
post_receipt: postReceipt,
|
|
380
|
-
envelope,
|
|
381
|
-
transform_artifact: transformArtifact,
|
|
382
|
-
patch_digest: patchDigest,
|
|
383
|
-
});
|
|
384
|
-
const failed = checks.filter(({ passed }) => !passed).map(({ id }) => id);
|
|
385
|
-
if (failed.length > 0) fail(`cross-binding failed: ${failed.join(', ')}`);
|
|
386
|
-
return envelope;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function failedEvaluation() {
|
|
390
|
-
const checks = [
|
|
391
|
-
'schema',
|
|
392
|
-
'compiler_identity',
|
|
393
|
-
'fixed_inputs',
|
|
394
|
-
'control_conflict',
|
|
395
|
-
'test_write_conflicts',
|
|
396
|
-
'treatment_parallel',
|
|
397
|
-
'unknowns',
|
|
398
|
-
'hard_precedence',
|
|
399
|
-
'negative_state',
|
|
400
|
-
'behavior_binding',
|
|
401
|
-
'portable_preimages',
|
|
402
|
-
'sanitized_diagnostics',
|
|
403
|
-
'predecessor',
|
|
404
|
-
'version_barrier',
|
|
405
|
-
'source_invariant',
|
|
406
|
-
].map((id) => ({ id, passed: false }));
|
|
407
|
-
return {
|
|
408
|
-
schema: 'lattice.rc1.hypothesis_evaluation.v3',
|
|
409
|
-
supported: false,
|
|
410
|
-
checks,
|
|
411
|
-
failed_conditions: checks.map(({ id }) => id),
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* comparison summaryのbehavior自己申告を使わず、underlying artifactからv5仮説を再評価する。
|
|
417
|
-
*/
|
|
418
|
-
export function evaluateRc1V5Hypothesis(options = {}) {
|
|
419
|
-
try {
|
|
420
|
-
if (!exactRecord(options, ['comparison', 'behaviorEvidence'])
|
|
421
|
-
|| !isPlainObject(options.comparison)) {
|
|
422
|
-
return failedEvaluation();
|
|
423
|
-
}
|
|
424
|
-
const { comparison, behaviorEvidence } = options;
|
|
425
|
-
const behaviorChecks = behaviorBindingChecks(behaviorEvidence);
|
|
426
|
-
const envelope = isPlainObject(behaviorEvidence) ? behaviorEvidence.envelope : null;
|
|
427
|
-
const comparisonBound = comparison.schema === 'lattice.rc1.control_treatment_comparison.v3'
|
|
428
|
-
&& exactRecord(comparison.behavior, ['evidence_envelope_digest'])
|
|
429
|
-
&& validateEnvelope(envelope)
|
|
430
|
-
&& comparison.behavior.evidence_envelope_digest === envelope.envelope_digest;
|
|
431
|
-
const behaviorPassed = comparisonBound && behaviorChecks.every(({ passed }) => passed);
|
|
432
|
-
const projection = structuredClone(comparison);
|
|
433
|
-
projection.schema = 'lattice.rc1.control_treatment_comparison.v2';
|
|
434
|
-
projection.behavior = {
|
|
435
|
-
control: {
|
|
436
|
-
outcome: behaviorPassed ? 'passed' : 'failed',
|
|
437
|
-
oracle_digest: behaviorEvidence?.pre_receipt?.oracle_digest ?? null,
|
|
438
|
-
},
|
|
439
|
-
treatment: {
|
|
440
|
-
outcome: behaviorPassed ? 'passed' : 'failed',
|
|
441
|
-
oracle_digest: behaviorEvidence?.post_receipt?.oracle_digest ?? null,
|
|
442
|
-
},
|
|
443
|
-
};
|
|
444
|
-
const v4 = evaluateRc1Hypothesis(projection);
|
|
445
|
-
const checks = v4.checks.map((check) => {
|
|
446
|
-
if (check.id === 'schema') {
|
|
447
|
-
return { id: 'schema', passed: comparison.schema === 'lattice.rc1.control_treatment_comparison.v3' };
|
|
448
|
-
}
|
|
449
|
-
if (check.id === 'behavior') return { id: 'behavior_binding', passed: behaviorPassed };
|
|
450
|
-
return check;
|
|
451
|
-
});
|
|
452
|
-
const failedConditions = checks.filter(({ passed }) => !passed).map(({ id }) => id);
|
|
453
|
-
return {
|
|
454
|
-
schema: 'lattice.rc1.hypothesis_evaluation.v3',
|
|
455
|
-
supported: failedConditions.length === 0,
|
|
456
|
-
checks,
|
|
457
|
-
failed_conditions: failedConditions,
|
|
458
|
-
};
|
|
459
|
-
} catch {
|
|
460
|
-
return failedEvaluation();
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function sha256(bytes) {
|
|
465
|
-
return createHash('sha256').update(bytes).digest('hex');
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function validateManifest(value) {
|
|
469
|
-
try {
|
|
470
|
-
canonicalizeArtifact(value);
|
|
471
|
-
} catch {
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
if (!exactRecord(value, ['schema', 'base_sha', 'result_digest', 'files'])
|
|
475
|
-
|| value.schema !== 'lattice.rc1.artifact_manifest.v5'
|
|
476
|
-
|| !GIT_SHA1.test(value.base_sha)
|
|
477
|
-
|| !digest(value.result_digest)
|
|
478
|
-
|| !Array.isArray(value.files)
|
|
479
|
-
|| value.files.length === 0) {
|
|
480
|
-
return false;
|
|
481
|
-
}
|
|
482
|
-
const paths = value.files.map(({ path: filePath }) => filePath);
|
|
483
|
-
return uniqueSortedStrings(paths)
|
|
484
|
-
&& value.files.every((file) => (
|
|
485
|
-
exactRecord(file, ['path', 'media_type', 'bytes', 'sha256'])
|
|
486
|
-
&& repoPath(file.path)
|
|
487
|
-
&& (file.media_type === 'application/json' || file.media_type === 'text/x-diff')
|
|
488
|
-
&& file.media_type === (file.path.endsWith('.json')
|
|
489
|
-
? 'application/json'
|
|
490
|
-
: 'text/x-diff')
|
|
491
|
-
&& Number.isSafeInteger(file.bytes)
|
|
492
|
-
&& file.bytes >= 0
|
|
493
|
-
&& digest(file.sha256)
|
|
494
|
-
));
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function payloadMap(payloads) {
|
|
498
|
-
if (!Array.isArray(payloads) || payloads.length === 0) return null;
|
|
499
|
-
const map = new Map();
|
|
500
|
-
for (const payload of payloads) {
|
|
501
|
-
if (!exactRecord(payload, ['path', 'bytes'])
|
|
502
|
-
|| !repoPath(payload.path)
|
|
503
|
-
|| !Buffer.isBuffer(payload.bytes)
|
|
504
|
-
|| map.has(payload.path)) {
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
map.set(payload.path, payload.bytes);
|
|
508
|
-
}
|
|
509
|
-
return map;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function parseJson(bytes) {
|
|
513
|
-
try {
|
|
514
|
-
const value = JSON.parse(bytes.toString('utf8'));
|
|
515
|
-
const serialized = Buffer.from(JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
516
|
-
return serialized.equals(bytes) ? value : null;
|
|
517
|
-
} catch {
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** manifestと保存bytesだけからrequired behavior artifactのhashとcross-bindingを検証する。 */
|
|
523
|
-
export function verifyRc1V5BehaviorArtifactSet(options = {}) {
|
|
524
|
-
try {
|
|
525
|
-
const manifest = exactRecord(options, ['manifest', 'payloads']) ? options.manifest : null;
|
|
526
|
-
const payloads = exactRecord(options, ['manifest', 'payloads'])
|
|
527
|
-
? payloadMap(options.payloads)
|
|
528
|
-
: null;
|
|
529
|
-
const manifestValid = validateManifest(manifest);
|
|
530
|
-
const manifestPaths = manifestValid ? manifest.files.map(({ path: filePath }) => filePath) : [];
|
|
531
|
-
const payloadPaths = payloads ? [...payloads.keys()].sort() : [];
|
|
532
|
-
const requiredPresent = REQUIRED_BEHAVIOR_PATHS.every((requiredPath) => (
|
|
533
|
-
manifestPaths.includes(requiredPath) && payloadPaths.includes(requiredPath)
|
|
534
|
-
));
|
|
535
|
-
const pathSetsMatch = manifestValid && payloads !== null
|
|
536
|
-
&& sameArray(manifestPaths, payloadPaths);
|
|
537
|
-
const bytesMatch = pathSetsMatch && manifest.files.every((file) => {
|
|
538
|
-
const bytes = payloads.get(file.path);
|
|
539
|
-
return bytes.byteLength === file.bytes && sha256(bytes) === file.sha256;
|
|
540
|
-
});
|
|
541
|
-
const pre = bytesMatch ? parseJson(payloads.get('behavior/pre-receipt.json')) : null;
|
|
542
|
-
const post = bytesMatch ? parseJson(payloads.get('behavior/post-receipt.json')) : null;
|
|
543
|
-
const envelope = bytesMatch ? parseJson(payloads.get('behavior/evidence-envelope.json')) : null;
|
|
544
|
-
const transform = bytesMatch ? parseJson(payloads.get('transform/transform-artifact.json')) : null;
|
|
545
|
-
const patch = bytesMatch ? payloads.get('transform/seam.patch') : null;
|
|
546
|
-
const parsed = [pre, post, envelope, transform].every((value) => value !== null)
|
|
547
|
-
&& Buffer.isBuffer(patch);
|
|
548
|
-
const bindingChecks = parsed ? behaviorBindingChecks({
|
|
549
|
-
pre_receipt: pre,
|
|
550
|
-
post_receipt: post,
|
|
551
|
-
envelope,
|
|
552
|
-
transform_artifact: transform,
|
|
553
|
-
patch_digest: sha256(patch),
|
|
554
|
-
}) : [];
|
|
555
|
-
const bindingPassed = parsed
|
|
556
|
-
&& bindingChecks.length > 0
|
|
557
|
-
&& bindingChecks.every(({ passed }) => passed);
|
|
558
|
-
const resultBound = bindingPassed
|
|
559
|
-
&& manifest.base_sha === envelope.base_sha
|
|
560
|
-
&& manifest.result_digest === envelope.envelope_digest;
|
|
561
|
-
const checks = [
|
|
562
|
-
{ id: 'manifest_schema', passed: manifestValid },
|
|
563
|
-
{ id: 'required_paths', passed: requiredPresent },
|
|
564
|
-
{ id: 'path_bijection', passed: pathSetsMatch },
|
|
565
|
-
{ id: 'byte_hashes', passed: bytesMatch },
|
|
566
|
-
{ id: 'payload_parsing', passed: parsed },
|
|
567
|
-
{ id: 'behavior_binding', passed: bindingPassed },
|
|
568
|
-
{ id: 'result_binding', passed: resultBound },
|
|
569
|
-
];
|
|
570
|
-
const failedConditions = checks.filter(({ passed }) => !passed).map(({ id }) => id);
|
|
571
|
-
return {
|
|
572
|
-
schema: 'lattice.rc1.behavior_artifact_verification.v1',
|
|
573
|
-
valid: failedConditions.length === 0,
|
|
574
|
-
checks,
|
|
575
|
-
failed_conditions: failedConditions,
|
|
576
|
-
};
|
|
577
|
-
} catch {
|
|
578
|
-
const checks = [
|
|
579
|
-
'manifest_schema',
|
|
580
|
-
'required_paths',
|
|
581
|
-
'path_bijection',
|
|
582
|
-
'byte_hashes',
|
|
583
|
-
'payload_parsing',
|
|
584
|
-
'behavior_binding',
|
|
585
|
-
'result_binding',
|
|
586
|
-
].map((id) => ({ id, passed: false }));
|
|
587
|
-
return {
|
|
588
|
-
schema: 'lattice.rc1.behavior_artifact_verification.v1',
|
|
589
|
-
valid: false,
|
|
590
|
-
checks,
|
|
591
|
-
failed_conditions: checks.map(({ id }) => id),
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
canonicalizeArtifact,
|
|
6
|
+
digestArtifact,
|
|
7
|
+
validateTransformArtifact,
|
|
8
|
+
} from './artifact-contracts.mjs';
|
|
9
|
+
import { evaluateRc1Hypothesis } from './rc1-comparison.mjs';
|
|
10
|
+
|
|
11
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
12
|
+
const GIT_SHA1 = /^[0-9a-f]{40}$/;
|
|
13
|
+
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
14
|
+
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
15
|
+
const REQUIRED_BEHAVIOR_PATHS = Object.freeze([
|
|
16
|
+
'behavior/evidence-envelope.json',
|
|
17
|
+
'behavior/post-receipt.json',
|
|
18
|
+
'behavior/pre-receipt.json',
|
|
19
|
+
'transform/seam.patch',
|
|
20
|
+
'transform/transform-artifact.json',
|
|
21
|
+
].sort());
|
|
22
|
+
const RECEIPT_KEYS = Object.freeze([
|
|
23
|
+
'schema',
|
|
24
|
+
'role',
|
|
25
|
+
'base_sha',
|
|
26
|
+
'oracle_digest',
|
|
27
|
+
'entrypoint',
|
|
28
|
+
'export_name',
|
|
29
|
+
'entrypoint_content_digest',
|
|
30
|
+
'surface',
|
|
31
|
+
'surface_digest',
|
|
32
|
+
'observation',
|
|
33
|
+
'outcome',
|
|
34
|
+
'case_results',
|
|
35
|
+
'receipt_digest',
|
|
36
|
+
]);
|
|
37
|
+
const ENVELOPE_KEYS = Object.freeze([
|
|
38
|
+
'schema',
|
|
39
|
+
'base_sha',
|
|
40
|
+
'oracle_digest',
|
|
41
|
+
'pre_receipt_digest',
|
|
42
|
+
'post_receipt_digest',
|
|
43
|
+
'pre_surface_digest',
|
|
44
|
+
'post_surface_digest',
|
|
45
|
+
'transform_artifact_digest',
|
|
46
|
+
'patch_digest',
|
|
47
|
+
'output_snapshot_digest',
|
|
48
|
+
'envelope_digest',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function isPlainObject(value) {
|
|
52
|
+
return value !== null
|
|
53
|
+
&& typeof value === 'object'
|
|
54
|
+
&& !Array.isArray(value)
|
|
55
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function exactRecord(value, keys) {
|
|
59
|
+
if (!isPlainObject(value)) return false;
|
|
60
|
+
const actual = Object.keys(value).sort();
|
|
61
|
+
const expected = [...keys].sort();
|
|
62
|
+
return actual.length === expected.length
|
|
63
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function repoPath(value) {
|
|
67
|
+
return typeof value === 'string'
|
|
68
|
+
&& value.length > 0
|
|
69
|
+
&& value === value.trim()
|
|
70
|
+
&& Buffer.byteLength(value, 'utf8') <= 1_024
|
|
71
|
+
&& !CONTROL_CHARACTER.test(value)
|
|
72
|
+
&& !path.posix.isAbsolute(value)
|
|
73
|
+
&& !value.startsWith('/')
|
|
74
|
+
&& !value.endsWith('/')
|
|
75
|
+
&& !value.includes('\\')
|
|
76
|
+
&& !/^[A-Za-z]:/.test(value)
|
|
77
|
+
&& value.split('/').every((segment) => (
|
|
78
|
+
segment !== '' && segment !== '.' && segment !== '..'
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function digest(value) {
|
|
83
|
+
return typeof value === 'string' && SHA256.test(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sameArray(left, right) {
|
|
87
|
+
return Array.isArray(left)
|
|
88
|
+
&& Array.isArray(right)
|
|
89
|
+
&& left.length === right.length
|
|
90
|
+
&& left.every((entry, index) => entry === right[index]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function uniqueSortedStrings(values) {
|
|
94
|
+
if (!Array.isArray(values) || values.length === 0) return false;
|
|
95
|
+
const sorted = [...values].sort();
|
|
96
|
+
return values.every((value, index) => (
|
|
97
|
+
typeof value === 'string'
|
|
98
|
+
&& value === sorted[index]
|
|
99
|
+
&& (index === 0 || value !== values[index - 1])
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function artifactDigestWithout(value, digestKey) {
|
|
104
|
+
const { [digestKey]: ignored, ...preimage } = value;
|
|
105
|
+
return digestArtifact(preimage);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validateSurface(value) {
|
|
109
|
+
try {
|
|
110
|
+
if (!exactRecord(value, ['schema', 'files'])
|
|
111
|
+
|| value.schema !== 'lattice.rc1.behavior_surface_snapshot.v1'
|
|
112
|
+
|| !Array.isArray(value.files)
|
|
113
|
+
|| value.files.length === 0
|
|
114
|
+
|| value.files.length > 128) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const paths = value.files.map(({ path: filePath }) => filePath);
|
|
118
|
+
if (!uniqueSortedStrings(paths)) return false;
|
|
119
|
+
for (const file of value.files) {
|
|
120
|
+
if (!exactRecord(file, ['path', 'state', 'content_digest'])
|
|
121
|
+
|| !repoPath(file.path)
|
|
122
|
+
|| (file.state !== 'present' && file.state !== 'absent')
|
|
123
|
+
|| (file.state === 'present' && !digest(file.content_digest))
|
|
124
|
+
|| (file.state === 'absent' && file.content_digest !== null)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
canonicalizeArtifact(value);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateCaseResult(value) {
|
|
136
|
+
return exactRecord(value, [
|
|
137
|
+
'id',
|
|
138
|
+
'outcome',
|
|
139
|
+
'observed_kind',
|
|
140
|
+
'expected_digest',
|
|
141
|
+
'observed_digest',
|
|
142
|
+
])
|
|
143
|
+
&& typeof value.id === 'string'
|
|
144
|
+
&& IDENTIFIER.test(value.id)
|
|
145
|
+
&& (value.outcome === 'passed' || value.outcome === 'failed')
|
|
146
|
+
&& (value.observed_kind === 'return' || value.observed_kind === 'throw')
|
|
147
|
+
&& digest(value.expected_digest)
|
|
148
|
+
&& digest(value.observed_digest);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateReceipt(value) {
|
|
152
|
+
if (!exactRecord(value, RECEIPT_KEYS)
|
|
153
|
+
|| value.schema !== 'lattice.rc1.black_box_behavior_receipt.v3'
|
|
154
|
+
|| (value.role !== 'pre' && value.role !== 'post')
|
|
155
|
+
|| !GIT_SHA1.test(value.base_sha)
|
|
156
|
+
|| !digest(value.oracle_digest)
|
|
157
|
+
|| !repoPath(value.entrypoint)
|
|
158
|
+
|| typeof value.export_name !== 'string'
|
|
159
|
+
|| !IDENTIFIER.test(value.export_name)
|
|
160
|
+
|| !digest(value.entrypoint_content_digest)
|
|
161
|
+
|| !validateSurface(value.surface)
|
|
162
|
+
|| !digest(value.surface_digest)
|
|
163
|
+
|| value.surface_digest !== digestArtifact(value.surface)
|
|
164
|
+
|| !exactRecord(value.observation, [
|
|
165
|
+
'before_surface_digest',
|
|
166
|
+
'after_surface_digest',
|
|
167
|
+
])
|
|
168
|
+
|| value.observation.before_surface_digest !== value.surface_digest
|
|
169
|
+
|| value.observation.after_surface_digest !== value.surface_digest
|
|
170
|
+
|| (value.outcome !== 'passed' && value.outcome !== 'failed')
|
|
171
|
+
|| !Array.isArray(value.case_results)
|
|
172
|
+
|| value.case_results.length === 0
|
|
173
|
+
|| value.case_results.length > 64
|
|
174
|
+
|| !value.case_results.every(validateCaseResult)
|
|
175
|
+
|| new Set(value.case_results.map(({ id }) => id)).size !== value.case_results.length
|
|
176
|
+
|| (value.outcome === 'passed') !== value.case_results.every(({ outcome }) => outcome === 'passed')
|
|
177
|
+
|| !digest(value.receipt_digest)
|
|
178
|
+
|| value.receipt_digest !== artifactDigestWithout(value, 'receipt_digest')) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const entrypoint = value.surface.files.find(({ path: filePath }) => filePath === value.entrypoint);
|
|
182
|
+
return entrypoint?.state === 'present'
|
|
183
|
+
&& entrypoint.content_digest === value.entrypoint_content_digest;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** @param {unknown} value @returns {boolean} */
|
|
187
|
+
export function validateRc1V5BehaviorSurface(value) {
|
|
188
|
+
return validateSurface(value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @param {unknown} value @returns {boolean} */
|
|
192
|
+
export function validateRc1V5BehaviorReceipt(value) {
|
|
193
|
+
return validateReceipt(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validateEnvelope(value) {
|
|
197
|
+
return exactRecord(value, ENVELOPE_KEYS)
|
|
198
|
+
&& value.schema === 'lattice.rc1.behavior_evidence_envelope.v1'
|
|
199
|
+
&& GIT_SHA1.test(value.base_sha)
|
|
200
|
+
&& [
|
|
201
|
+
value.oracle_digest,
|
|
202
|
+
value.pre_receipt_digest,
|
|
203
|
+
value.post_receipt_digest,
|
|
204
|
+
value.pre_surface_digest,
|
|
205
|
+
value.post_surface_digest,
|
|
206
|
+
value.transform_artifact_digest,
|
|
207
|
+
value.patch_digest,
|
|
208
|
+
value.output_snapshot_digest,
|
|
209
|
+
value.envelope_digest,
|
|
210
|
+
].every(digest)
|
|
211
|
+
&& value.envelope_digest === artifactDigestWithout(value, 'envelope_digest');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function surfacePaths(surface) {
|
|
215
|
+
return validateSurface(surface) ? surface.files.map(({ path: filePath }) => filePath) : [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function outputProjection(surface) {
|
|
219
|
+
if (!validateSurface(surface) || surface.files.some(({ state }) => state !== 'present')) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
files: surface.files.map(({ path: filePath, content_digest: contentDigest }) => ({
|
|
224
|
+
path: filePath,
|
|
225
|
+
content_digest: contentDigest,
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validAcceptedTransform(value) {
|
|
231
|
+
if (!validateTransformArtifact(value)
|
|
232
|
+
|| value.status !== 'accepted'
|
|
233
|
+
|| !GIT_SHA1.test(value.source?.base_sha)
|
|
234
|
+
|| !digest(value.patch?.digest)
|
|
235
|
+
|| !digest(value.output?.snapshot_digest)
|
|
236
|
+
|| !Array.isArray(value.output?.files)
|
|
237
|
+
|| value.output.files.length === 0) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const paths = value.output.files.map(({ path: filePath }) => filePath);
|
|
241
|
+
return uniqueSortedStrings(paths)
|
|
242
|
+
&& value.output.files.every((file) => (
|
|
243
|
+
exactRecord(file, ['path', 'content_digest'])
|
|
244
|
+
&& repoPath(file.path)
|
|
245
|
+
&& digest(file.content_digest)
|
|
246
|
+
))
|
|
247
|
+
&& value.output.snapshot_digest === digestArtifact({ files: value.output.files });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function behaviorBindingChecks(value) {
|
|
251
|
+
const validInput = exactRecord(value, [
|
|
252
|
+
'pre_receipt',
|
|
253
|
+
'post_receipt',
|
|
254
|
+
'envelope',
|
|
255
|
+
'transform_artifact',
|
|
256
|
+
'patch_digest',
|
|
257
|
+
]);
|
|
258
|
+
const pre = validInput ? value.pre_receipt : null;
|
|
259
|
+
const post = validInput ? value.post_receipt : null;
|
|
260
|
+
const envelope = validInput ? value.envelope : null;
|
|
261
|
+
const transform = validInput ? value.transform_artifact : null;
|
|
262
|
+
const patchDigest = validInput ? value.patch_digest : null;
|
|
263
|
+
const preValid = validateReceipt(pre);
|
|
264
|
+
const postValid = validateReceipt(post);
|
|
265
|
+
const envelopeValid = validateEnvelope(envelope);
|
|
266
|
+
const transformValid = validAcceptedTransform(transform);
|
|
267
|
+
const prePaths = preValid ? surfacePaths(pre.surface) : [];
|
|
268
|
+
const postPaths = postValid ? surfacePaths(post.surface) : [];
|
|
269
|
+
const projection = postValid ? outputProjection(post.surface) : null;
|
|
270
|
+
const preSourceBound = preValid
|
|
271
|
+
&& transformValid
|
|
272
|
+
&& pre.surface_digest === transform.source.code_snapshot_digest;
|
|
273
|
+
const outputBound = transformValid
|
|
274
|
+
&& projection !== null
|
|
275
|
+
&& sameArray(prePaths, postPaths)
|
|
276
|
+
&& sameArray(postPaths, transform.scope.allowed_paths)
|
|
277
|
+
&& digestArtifact(projection) === transform.output.snapshot_digest
|
|
278
|
+
&& digestArtifact(projection.files) === digestArtifact(transform.output.files);
|
|
279
|
+
|
|
280
|
+
return [
|
|
281
|
+
{ id: 'receipt_schema', passed: preValid && postValid },
|
|
282
|
+
{
|
|
283
|
+
id: 'receipt_identity',
|
|
284
|
+
passed: preValid && postValid
|
|
285
|
+
&& pre.role === 'pre'
|
|
286
|
+
&& post.role === 'post'
|
|
287
|
+
&& pre.receipt_digest !== post.receipt_digest,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'base_binding',
|
|
291
|
+
passed: preValid && postValid && transformValid
|
|
292
|
+
&& pre.base_sha === post.base_sha
|
|
293
|
+
&& pre.base_sha === transform.source.base_sha,
|
|
294
|
+
},
|
|
295
|
+
{ id: 'pre_source_binding', passed: preSourceBound },
|
|
296
|
+
{
|
|
297
|
+
id: 'oracle_binding',
|
|
298
|
+
passed: preValid && postValid
|
|
299
|
+
&& pre.oracle_digest === post.oracle_digest
|
|
300
|
+
&& pre.entrypoint === post.entrypoint
|
|
301
|
+
&& pre.export_name === post.export_name,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: 'behavior_outcome',
|
|
305
|
+
passed: preValid && postValid && pre.outcome === 'passed' && post.outcome === 'passed',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'surface_binding',
|
|
309
|
+
passed: preValid && postValid && sameArray(prePaths, postPaths),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'transform_binding',
|
|
313
|
+
passed: transformValid && envelopeValid
|
|
314
|
+
&& envelope.transform_artifact_digest === digestArtifact(transform),
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: 'patch_binding',
|
|
318
|
+
passed: transformValid && envelopeValid && digest(patchDigest)
|
|
319
|
+
&& patchDigest === transform.patch.digest
|
|
320
|
+
&& patchDigest === envelope.patch_digest,
|
|
321
|
+
},
|
|
322
|
+
{ id: 'post_output_binding', passed: outputBound },
|
|
323
|
+
{
|
|
324
|
+
id: 'envelope_binding',
|
|
325
|
+
passed: preValid && postValid && transformValid && envelopeValid && outputBound
|
|
326
|
+
&& envelope.base_sha === pre.base_sha
|
|
327
|
+
&& envelope.oracle_digest === pre.oracle_digest
|
|
328
|
+
&& envelope.pre_receipt_digest === pre.receipt_digest
|
|
329
|
+
&& envelope.post_receipt_digest === post.receipt_digest
|
|
330
|
+
&& envelope.pre_surface_digest === pre.surface_digest
|
|
331
|
+
&& envelope.post_surface_digest === post.surface_digest
|
|
332
|
+
&& envelope.output_snapshot_digest === transform.output.snapshot_digest,
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function fail(reason) {
|
|
338
|
+
throw new TypeError(`RC1 v5 behavior evidence契約違反: ${reason}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* full pre/post receiptをaccepted transformとpatchへcross-bindする。
|
|
343
|
+
*/
|
|
344
|
+
export function compileRc1V5BehaviorEvidence(options = {}) {
|
|
345
|
+
if (!exactRecord(options, [
|
|
346
|
+
'preReceipt',
|
|
347
|
+
'postReceipt',
|
|
348
|
+
'transformArtifact',
|
|
349
|
+
'patchDigest',
|
|
350
|
+
])) {
|
|
351
|
+
fail('input shapeが不正');
|
|
352
|
+
}
|
|
353
|
+
const {
|
|
354
|
+
preReceipt,
|
|
355
|
+
postReceipt,
|
|
356
|
+
transformArtifact,
|
|
357
|
+
patchDigest,
|
|
358
|
+
} = options;
|
|
359
|
+
if (!validateReceipt(preReceipt) || !validateReceipt(postReceipt)) fail('full receiptが不正');
|
|
360
|
+
if (!validAcceptedTransform(transformArtifact)) fail('accepted transform artifactが不正');
|
|
361
|
+
if (!digest(patchDigest) || patchDigest !== transformArtifact.patch.digest) {
|
|
362
|
+
fail('patch digestがaccepted transformと不一致');
|
|
363
|
+
}
|
|
364
|
+
const preimage = {
|
|
365
|
+
schema: 'lattice.rc1.behavior_evidence_envelope.v1',
|
|
366
|
+
base_sha: preReceipt.base_sha,
|
|
367
|
+
oracle_digest: preReceipt.oracle_digest,
|
|
368
|
+
pre_receipt_digest: preReceipt.receipt_digest,
|
|
369
|
+
post_receipt_digest: postReceipt.receipt_digest,
|
|
370
|
+
pre_surface_digest: preReceipt.surface_digest,
|
|
371
|
+
post_surface_digest: postReceipt.surface_digest,
|
|
372
|
+
transform_artifact_digest: digestArtifact(transformArtifact),
|
|
373
|
+
patch_digest: patchDigest,
|
|
374
|
+
output_snapshot_digest: transformArtifact.output.snapshot_digest,
|
|
375
|
+
};
|
|
376
|
+
const envelope = { ...preimage, envelope_digest: digestArtifact(preimage) };
|
|
377
|
+
const checks = behaviorBindingChecks({
|
|
378
|
+
pre_receipt: preReceipt,
|
|
379
|
+
post_receipt: postReceipt,
|
|
380
|
+
envelope,
|
|
381
|
+
transform_artifact: transformArtifact,
|
|
382
|
+
patch_digest: patchDigest,
|
|
383
|
+
});
|
|
384
|
+
const failed = checks.filter(({ passed }) => !passed).map(({ id }) => id);
|
|
385
|
+
if (failed.length > 0) fail(`cross-binding failed: ${failed.join(', ')}`);
|
|
386
|
+
return envelope;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function failedEvaluation() {
|
|
390
|
+
const checks = [
|
|
391
|
+
'schema',
|
|
392
|
+
'compiler_identity',
|
|
393
|
+
'fixed_inputs',
|
|
394
|
+
'control_conflict',
|
|
395
|
+
'test_write_conflicts',
|
|
396
|
+
'treatment_parallel',
|
|
397
|
+
'unknowns',
|
|
398
|
+
'hard_precedence',
|
|
399
|
+
'negative_state',
|
|
400
|
+
'behavior_binding',
|
|
401
|
+
'portable_preimages',
|
|
402
|
+
'sanitized_diagnostics',
|
|
403
|
+
'predecessor',
|
|
404
|
+
'version_barrier',
|
|
405
|
+
'source_invariant',
|
|
406
|
+
].map((id) => ({ id, passed: false }));
|
|
407
|
+
return {
|
|
408
|
+
schema: 'lattice.rc1.hypothesis_evaluation.v3',
|
|
409
|
+
supported: false,
|
|
410
|
+
checks,
|
|
411
|
+
failed_conditions: checks.map(({ id }) => id),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* comparison summaryのbehavior自己申告を使わず、underlying artifactからv5仮説を再評価する。
|
|
417
|
+
*/
|
|
418
|
+
export function evaluateRc1V5Hypothesis(options = {}) {
|
|
419
|
+
try {
|
|
420
|
+
if (!exactRecord(options, ['comparison', 'behaviorEvidence'])
|
|
421
|
+
|| !isPlainObject(options.comparison)) {
|
|
422
|
+
return failedEvaluation();
|
|
423
|
+
}
|
|
424
|
+
const { comparison, behaviorEvidence } = options;
|
|
425
|
+
const behaviorChecks = behaviorBindingChecks(behaviorEvidence);
|
|
426
|
+
const envelope = isPlainObject(behaviorEvidence) ? behaviorEvidence.envelope : null;
|
|
427
|
+
const comparisonBound = comparison.schema === 'lattice.rc1.control_treatment_comparison.v3'
|
|
428
|
+
&& exactRecord(comparison.behavior, ['evidence_envelope_digest'])
|
|
429
|
+
&& validateEnvelope(envelope)
|
|
430
|
+
&& comparison.behavior.evidence_envelope_digest === envelope.envelope_digest;
|
|
431
|
+
const behaviorPassed = comparisonBound && behaviorChecks.every(({ passed }) => passed);
|
|
432
|
+
const projection = structuredClone(comparison);
|
|
433
|
+
projection.schema = 'lattice.rc1.control_treatment_comparison.v2';
|
|
434
|
+
projection.behavior = {
|
|
435
|
+
control: {
|
|
436
|
+
outcome: behaviorPassed ? 'passed' : 'failed',
|
|
437
|
+
oracle_digest: behaviorEvidence?.pre_receipt?.oracle_digest ?? null,
|
|
438
|
+
},
|
|
439
|
+
treatment: {
|
|
440
|
+
outcome: behaviorPassed ? 'passed' : 'failed',
|
|
441
|
+
oracle_digest: behaviorEvidence?.post_receipt?.oracle_digest ?? null,
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
const v4 = evaluateRc1Hypothesis(projection);
|
|
445
|
+
const checks = v4.checks.map((check) => {
|
|
446
|
+
if (check.id === 'schema') {
|
|
447
|
+
return { id: 'schema', passed: comparison.schema === 'lattice.rc1.control_treatment_comparison.v3' };
|
|
448
|
+
}
|
|
449
|
+
if (check.id === 'behavior') return { id: 'behavior_binding', passed: behaviorPassed };
|
|
450
|
+
return check;
|
|
451
|
+
});
|
|
452
|
+
const failedConditions = checks.filter(({ passed }) => !passed).map(({ id }) => id);
|
|
453
|
+
return {
|
|
454
|
+
schema: 'lattice.rc1.hypothesis_evaluation.v3',
|
|
455
|
+
supported: failedConditions.length === 0,
|
|
456
|
+
checks,
|
|
457
|
+
failed_conditions: failedConditions,
|
|
458
|
+
};
|
|
459
|
+
} catch {
|
|
460
|
+
return failedEvaluation();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function sha256(bytes) {
|
|
465
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function validateManifest(value) {
|
|
469
|
+
try {
|
|
470
|
+
canonicalizeArtifact(value);
|
|
471
|
+
} catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
if (!exactRecord(value, ['schema', 'base_sha', 'result_digest', 'files'])
|
|
475
|
+
|| value.schema !== 'lattice.rc1.artifact_manifest.v5'
|
|
476
|
+
|| !GIT_SHA1.test(value.base_sha)
|
|
477
|
+
|| !digest(value.result_digest)
|
|
478
|
+
|| !Array.isArray(value.files)
|
|
479
|
+
|| value.files.length === 0) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
const paths = value.files.map(({ path: filePath }) => filePath);
|
|
483
|
+
return uniqueSortedStrings(paths)
|
|
484
|
+
&& value.files.every((file) => (
|
|
485
|
+
exactRecord(file, ['path', 'media_type', 'bytes', 'sha256'])
|
|
486
|
+
&& repoPath(file.path)
|
|
487
|
+
&& (file.media_type === 'application/json' || file.media_type === 'text/x-diff')
|
|
488
|
+
&& file.media_type === (file.path.endsWith('.json')
|
|
489
|
+
? 'application/json'
|
|
490
|
+
: 'text/x-diff')
|
|
491
|
+
&& Number.isSafeInteger(file.bytes)
|
|
492
|
+
&& file.bytes >= 0
|
|
493
|
+
&& digest(file.sha256)
|
|
494
|
+
));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function payloadMap(payloads) {
|
|
498
|
+
if (!Array.isArray(payloads) || payloads.length === 0) return null;
|
|
499
|
+
const map = new Map();
|
|
500
|
+
for (const payload of payloads) {
|
|
501
|
+
if (!exactRecord(payload, ['path', 'bytes'])
|
|
502
|
+
|| !repoPath(payload.path)
|
|
503
|
+
|| !Buffer.isBuffer(payload.bytes)
|
|
504
|
+
|| map.has(payload.path)) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
map.set(payload.path, payload.bytes);
|
|
508
|
+
}
|
|
509
|
+
return map;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function parseJson(bytes) {
|
|
513
|
+
try {
|
|
514
|
+
const value = JSON.parse(bytes.toString('utf8'));
|
|
515
|
+
const serialized = Buffer.from(JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
516
|
+
return serialized.equals(bytes) ? value : null;
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** manifestと保存bytesだけからrequired behavior artifactのhashとcross-bindingを検証する。 */
|
|
523
|
+
export function verifyRc1V5BehaviorArtifactSet(options = {}) {
|
|
524
|
+
try {
|
|
525
|
+
const manifest = exactRecord(options, ['manifest', 'payloads']) ? options.manifest : null;
|
|
526
|
+
const payloads = exactRecord(options, ['manifest', 'payloads'])
|
|
527
|
+
? payloadMap(options.payloads)
|
|
528
|
+
: null;
|
|
529
|
+
const manifestValid = validateManifest(manifest);
|
|
530
|
+
const manifestPaths = manifestValid ? manifest.files.map(({ path: filePath }) => filePath) : [];
|
|
531
|
+
const payloadPaths = payloads ? [...payloads.keys()].sort() : [];
|
|
532
|
+
const requiredPresent = REQUIRED_BEHAVIOR_PATHS.every((requiredPath) => (
|
|
533
|
+
manifestPaths.includes(requiredPath) && payloadPaths.includes(requiredPath)
|
|
534
|
+
));
|
|
535
|
+
const pathSetsMatch = manifestValid && payloads !== null
|
|
536
|
+
&& sameArray(manifestPaths, payloadPaths);
|
|
537
|
+
const bytesMatch = pathSetsMatch && manifest.files.every((file) => {
|
|
538
|
+
const bytes = payloads.get(file.path);
|
|
539
|
+
return bytes.byteLength === file.bytes && sha256(bytes) === file.sha256;
|
|
540
|
+
});
|
|
541
|
+
const pre = bytesMatch ? parseJson(payloads.get('behavior/pre-receipt.json')) : null;
|
|
542
|
+
const post = bytesMatch ? parseJson(payloads.get('behavior/post-receipt.json')) : null;
|
|
543
|
+
const envelope = bytesMatch ? parseJson(payloads.get('behavior/evidence-envelope.json')) : null;
|
|
544
|
+
const transform = bytesMatch ? parseJson(payloads.get('transform/transform-artifact.json')) : null;
|
|
545
|
+
const patch = bytesMatch ? payloads.get('transform/seam.patch') : null;
|
|
546
|
+
const parsed = [pre, post, envelope, transform].every((value) => value !== null)
|
|
547
|
+
&& Buffer.isBuffer(patch);
|
|
548
|
+
const bindingChecks = parsed ? behaviorBindingChecks({
|
|
549
|
+
pre_receipt: pre,
|
|
550
|
+
post_receipt: post,
|
|
551
|
+
envelope,
|
|
552
|
+
transform_artifact: transform,
|
|
553
|
+
patch_digest: sha256(patch),
|
|
554
|
+
}) : [];
|
|
555
|
+
const bindingPassed = parsed
|
|
556
|
+
&& bindingChecks.length > 0
|
|
557
|
+
&& bindingChecks.every(({ passed }) => passed);
|
|
558
|
+
const resultBound = bindingPassed
|
|
559
|
+
&& manifest.base_sha === envelope.base_sha
|
|
560
|
+
&& manifest.result_digest === envelope.envelope_digest;
|
|
561
|
+
const checks = [
|
|
562
|
+
{ id: 'manifest_schema', passed: manifestValid },
|
|
563
|
+
{ id: 'required_paths', passed: requiredPresent },
|
|
564
|
+
{ id: 'path_bijection', passed: pathSetsMatch },
|
|
565
|
+
{ id: 'byte_hashes', passed: bytesMatch },
|
|
566
|
+
{ id: 'payload_parsing', passed: parsed },
|
|
567
|
+
{ id: 'behavior_binding', passed: bindingPassed },
|
|
568
|
+
{ id: 'result_binding', passed: resultBound },
|
|
569
|
+
];
|
|
570
|
+
const failedConditions = checks.filter(({ passed }) => !passed).map(({ id }) => id);
|
|
571
|
+
return {
|
|
572
|
+
schema: 'lattice.rc1.behavior_artifact_verification.v1',
|
|
573
|
+
valid: failedConditions.length === 0,
|
|
574
|
+
checks,
|
|
575
|
+
failed_conditions: failedConditions,
|
|
576
|
+
};
|
|
577
|
+
} catch {
|
|
578
|
+
const checks = [
|
|
579
|
+
'manifest_schema',
|
|
580
|
+
'required_paths',
|
|
581
|
+
'path_bijection',
|
|
582
|
+
'byte_hashes',
|
|
583
|
+
'payload_parsing',
|
|
584
|
+
'behavior_binding',
|
|
585
|
+
'result_binding',
|
|
586
|
+
].map((id) => ({ id, passed: false }));
|
|
587
|
+
return {
|
|
588
|
+
schema: 'lattice.rc1.behavior_artifact_verification.v1',
|
|
589
|
+
valid: false,
|
|
590
|
+
checks,
|
|
591
|
+
failed_conditions: checks.map(({ id }) => id),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|