@davesheffer/hunch 1.22.2 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/contracts/change-proof/hunch.change-proof.v1.example.json +96 -0
- package/contracts/change-proof/hunch.change-proof.v1.schema.json +215 -0
- package/dist/changeProof.d.ts +1 -0
- package/dist/changeProof.js +2 -0
- package/dist/cli/index.js +37 -0
- package/dist/core/changeProof.js +512 -0
- package/dist/core/changeProofContract.d.ts +164 -0
- package/dist/core/changeProofContract.js +253 -0
- package/dist/core/projectDnaOutcomeExperience.d.ts +105 -0
- package/dist/core/projectDnaOutcomeExperience.js +271 -0
- package/dist/mcp/server.js +28 -0
- package/dist/projectDna.d.ts +1 -0
- package/dist/projectDna.js +1 -0
- package/package.json +8 -1
- package/server.json +2 -2
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { TextDecoder } from "node:util";
|
|
3
|
+
import { scanRepo } from "../extractors/indexer.js";
|
|
4
|
+
import { foreignRepoEnv, gitNullDevice } from "../extractors/git.js";
|
|
5
|
+
import { deriveChangeIdentity } from "./changeIdentity.js";
|
|
6
|
+
import { checkConformance } from "./conformance.js";
|
|
7
|
+
import { compareCodeUnits } from "./canonicalOrder.js";
|
|
8
|
+
import { pathMatchesGlob, pathsRelated } from "./glob.js";
|
|
9
|
+
import { discoverProjectDna } from "./projectDna.js";
|
|
10
|
+
import { isInForce } from "./topics.js";
|
|
11
|
+
import { HUNCH_VERSION } from "./version.js";
|
|
12
|
+
import { CHANGE_PROOF_ALGORITHM, CHANGE_PROOF_SCHEMA_VERSION, canonicalChangeProofJson, changeProofHash, sealChangeProof, } from "./changeProofContract.js";
|
|
13
|
+
const MAX_GIT_OUTPUT = 64 * 1024 * 1024;
|
|
14
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
15
|
+
const MAX_CHANGED_FILES = 2_048;
|
|
16
|
+
const MAX_BLAST_RADIUS = 4_096;
|
|
17
|
+
const MAX_INTERNAL_BLAST_RADIUS = 20_000;
|
|
18
|
+
const MAX_RECORDS = 1_024;
|
|
19
|
+
const MAX_RECORD_PATHS = 128;
|
|
20
|
+
const MAX_CONFORMANCE = 1_024;
|
|
21
|
+
const MAX_GAPS = 64;
|
|
22
|
+
const TRAVERSABLE = new Set(["calls", "depends_on", "imports", "contains", "implements"]);
|
|
23
|
+
function gitEnvironment() {
|
|
24
|
+
return {
|
|
25
|
+
...foreignRepoEnv(process.env),
|
|
26
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
27
|
+
GIT_CONFIG_GLOBAL: gitNullDevice(),
|
|
28
|
+
GIT_NO_REPLACE_OBJECTS: "1",
|
|
29
|
+
LC_ALL: "C",
|
|
30
|
+
LANG: "C",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function gitBytes(root, args) {
|
|
34
|
+
try {
|
|
35
|
+
return execFileSync("git", ["-C", root, ...args], {
|
|
36
|
+
encoding: "buffer",
|
|
37
|
+
env: gitEnvironment(),
|
|
38
|
+
maxBuffer: MAX_GIT_OUTPUT,
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
timeout: 30_000,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const stderr = error.stderr?.toString("utf8").trim().replace(/[\r\n]+/g, " ");
|
|
45
|
+
throw new Error(`could not derive native change proof${stderr ? `: ${stderr.slice(0, 500)}` : ""}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function gitText(root, args) {
|
|
49
|
+
return gitBytes(root, args).toString("utf8").trim();
|
|
50
|
+
}
|
|
51
|
+
function canonicalPath(path) {
|
|
52
|
+
if (!path || path.length > 4_096 || path.includes("\0") || path.includes("\\")
|
|
53
|
+
|| path.startsWith("/") || /^[A-Za-z]:/.test(path))
|
|
54
|
+
return false;
|
|
55
|
+
return !path.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
|
|
56
|
+
}
|
|
57
|
+
function exactChangedPaths(root, baseRevision, resultRevision) {
|
|
58
|
+
const raw = gitBytes(root, [
|
|
59
|
+
"diff", "--name-only", "-z", "--no-renames", "--no-ext-diff", "--no-textconv",
|
|
60
|
+
baseRevision, resultRevision, "--",
|
|
61
|
+
]);
|
|
62
|
+
const parts = [];
|
|
63
|
+
let start = 0;
|
|
64
|
+
for (let index = 0; index < raw.length; index++) {
|
|
65
|
+
if (raw[index] !== 0)
|
|
66
|
+
continue;
|
|
67
|
+
parts.push(raw.subarray(start, index));
|
|
68
|
+
start = index + 1;
|
|
69
|
+
}
|
|
70
|
+
if (!raw.length || raw[raw.length - 1] !== 0 || start !== raw.length || parts.some((part) => part.length < 1)) {
|
|
71
|
+
throw new Error("native change proof received an invalid exact Git path inventory");
|
|
72
|
+
}
|
|
73
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
74
|
+
const paths = [];
|
|
75
|
+
const invalid = [];
|
|
76
|
+
for (const [index, part] of parts.entries()) {
|
|
77
|
+
try {
|
|
78
|
+
const path = decoder.decode(part);
|
|
79
|
+
if (!canonicalPath(path))
|
|
80
|
+
throw new Error("unsafe path");
|
|
81
|
+
paths.push(path);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
invalid.push({ index, path_hash: changeProofHash(part.toString("base64")) });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const sorted = [...new Set(paths)].sort(compareCodeUnits);
|
|
88
|
+
if (sorted.length !== paths.length)
|
|
89
|
+
throw new Error("native change proof received duplicate Git paths");
|
|
90
|
+
return {
|
|
91
|
+
paths: sorted,
|
|
92
|
+
gaps: invalid.length ? [{ code: "changed_path_unrepresentable", values: invalid }] : [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function exactDiff(root, baseRevision, resultRevision) {
|
|
96
|
+
const raw = gitBytes(root, [
|
|
97
|
+
"diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "--unified=2",
|
|
98
|
+
baseRevision, resultRevision, "--",
|
|
99
|
+
]);
|
|
100
|
+
if (raw.byteLength <= MAX_DIFF_BYTES)
|
|
101
|
+
return { diff: raw.toString("utf8"), gaps: [] };
|
|
102
|
+
return {
|
|
103
|
+
diff: raw.subarray(0, MAX_DIFF_BYTES).toString("utf8"),
|
|
104
|
+
gaps: [{
|
|
105
|
+
code: "guard_diff_truncated",
|
|
106
|
+
values: [{ byte_count: raw.byteLength, content_hash: changeProofHash(raw) }],
|
|
107
|
+
}],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function topologyHash(scan) {
|
|
111
|
+
return changeProofHash({
|
|
112
|
+
symbols: scan.symbols
|
|
113
|
+
.map((symbol) => ({
|
|
114
|
+
id: symbol.id,
|
|
115
|
+
file: symbol.file,
|
|
116
|
+
name: symbol.name,
|
|
117
|
+
kind: symbol.kind,
|
|
118
|
+
signature_hash: symbol.signature_hash,
|
|
119
|
+
}))
|
|
120
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
121
|
+
edges: scan.edges
|
|
122
|
+
.map((edge) => ({ id: edge.id, from: edge.from, to: edge.to, type: edge.type }))
|
|
123
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
124
|
+
components: scan.components
|
|
125
|
+
.map((component) => ({ id: component.id, paths: [...component.paths].sort(compareCodeUnits) }))
|
|
126
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function graphSeal(scan) {
|
|
130
|
+
if (scan.source.kind !== "commit" || !scan.source.revision) {
|
|
131
|
+
throw new Error("native change proof requires an exact committed semantic graph");
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
source: "commit",
|
|
135
|
+
revision: scan.source.revision,
|
|
136
|
+
source_hash: scan.source.content_hash,
|
|
137
|
+
topology_hash: topologyHash(scan),
|
|
138
|
+
files: scan.result.files,
|
|
139
|
+
symbols: scan.result.symbols,
|
|
140
|
+
edges: scan.result.edges,
|
|
141
|
+
components: scan.result.components,
|
|
142
|
+
issue_count: scan.issues.length,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function graphBlast(scan, changedPaths, graph) {
|
|
146
|
+
const symbolById = new Map(scan.symbols.map((symbol) => [symbol.id, symbol]));
|
|
147
|
+
const symbolsByFile = new Map();
|
|
148
|
+
for (const symbol of scan.symbols) {
|
|
149
|
+
const ids = symbolsByFile.get(symbol.file) ?? [];
|
|
150
|
+
ids.push(symbol.id);
|
|
151
|
+
symbolsByFile.set(symbol.file, ids);
|
|
152
|
+
}
|
|
153
|
+
const incoming = new Map();
|
|
154
|
+
for (const edge of scan.edges) {
|
|
155
|
+
if (!TRAVERSABLE.has(edge.type))
|
|
156
|
+
continue;
|
|
157
|
+
const ids = incoming.get(edge.to) ?? [];
|
|
158
|
+
ids.push(edge.from);
|
|
159
|
+
incoming.set(edge.to, ids);
|
|
160
|
+
}
|
|
161
|
+
for (const ids of incoming.values())
|
|
162
|
+
ids.sort(compareCodeUnits);
|
|
163
|
+
const out = new Map();
|
|
164
|
+
for (const sourcePath of changedPaths) {
|
|
165
|
+
const starts = [...(symbolsByFile.get(sourcePath) ?? [])].sort(compareCodeUnits);
|
|
166
|
+
const seen = new Map(starts.map((id) => [id, 0]));
|
|
167
|
+
const queue = starts.map((id) => ({ id, depth: 0 }));
|
|
168
|
+
while (queue.length) {
|
|
169
|
+
const current = queue.shift();
|
|
170
|
+
if (current.depth >= 4)
|
|
171
|
+
continue;
|
|
172
|
+
for (const dependentId of incoming.get(current.id) ?? []) {
|
|
173
|
+
const depth = current.depth + 1;
|
|
174
|
+
const previous = seen.get(dependentId);
|
|
175
|
+
if (previous === undefined || depth < previous) {
|
|
176
|
+
seen.set(dependentId, depth);
|
|
177
|
+
queue.push({ id: dependentId, depth });
|
|
178
|
+
}
|
|
179
|
+
const dependent = symbolById.get(dependentId);
|
|
180
|
+
if (!dependent || dependent.file === sourcePath)
|
|
181
|
+
continue;
|
|
182
|
+
const key = `${sourcePath}\0${dependent.file}`;
|
|
183
|
+
const existing = out.get(key);
|
|
184
|
+
if (!existing || depth < existing.depth) {
|
|
185
|
+
out.set(key, {
|
|
186
|
+
source_path: sourcePath,
|
|
187
|
+
dependent_path: dependent.file,
|
|
188
|
+
depth,
|
|
189
|
+
graphs: [graph],
|
|
190
|
+
graph,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return [...out.values()];
|
|
197
|
+
}
|
|
198
|
+
function mergeBlast(base, result) {
|
|
199
|
+
const merged = new Map();
|
|
200
|
+
for (const entry of [...base, ...result]) {
|
|
201
|
+
const key = `${entry.source_path}\0${entry.dependent_path}`;
|
|
202
|
+
const current = merged.get(key);
|
|
203
|
+
if (!current) {
|
|
204
|
+
merged.set(key, {
|
|
205
|
+
source_path: entry.source_path,
|
|
206
|
+
dependent_path: entry.dependent_path,
|
|
207
|
+
depth: entry.depth,
|
|
208
|
+
graphs: [entry.graph],
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
current.depth = Math.min(current.depth, entry.depth);
|
|
213
|
+
current.graphs = [...new Set([...current.graphs, entry.graph])].sort(compareCodeUnits);
|
|
214
|
+
}
|
|
215
|
+
return sortCanonical([...merged.values()]);
|
|
216
|
+
}
|
|
217
|
+
function scopeMatches(path, scope) {
|
|
218
|
+
return pathMatchesGlob(path, scope) || pathMatchesGlob(scope, path) || pathsRelated(path, scope);
|
|
219
|
+
}
|
|
220
|
+
function decisionMatchesPath(decision, path, scans) {
|
|
221
|
+
if (decision.related_files.some((related) => scopeMatches(path, related)))
|
|
222
|
+
return true;
|
|
223
|
+
for (const componentId of decision.related_components) {
|
|
224
|
+
for (const scan of scans) {
|
|
225
|
+
const component = scan.components.find((candidate) => candidate.id === componentId);
|
|
226
|
+
if (component?.paths.some((scope) => scopeMatches(path, scope)))
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function constraintMatchesPath(constraint, path) {
|
|
233
|
+
return constraint.scope.some((scope) => scopeMatches(path, scope));
|
|
234
|
+
}
|
|
235
|
+
function lastChangeAt(root, revision, path) {
|
|
236
|
+
return gitText(root, ["log", "-1", "--format=%aI", revision, "--", path]);
|
|
237
|
+
}
|
|
238
|
+
function sortedUnique(values) {
|
|
239
|
+
return [...new Set(values)].sort(compareCodeUnits);
|
|
240
|
+
}
|
|
241
|
+
function sortCanonical(values) {
|
|
242
|
+
return values.sort((left, right) => compareCodeUnits(canonicalChangeProofJson(left), canonicalChangeProofJson(right)));
|
|
243
|
+
}
|
|
244
|
+
function addGap(target, code, values) {
|
|
245
|
+
if (!values.length)
|
|
246
|
+
return;
|
|
247
|
+
target.push({ code, values });
|
|
248
|
+
}
|
|
249
|
+
function sealedGaps(gaps) {
|
|
250
|
+
const merged = new Map();
|
|
251
|
+
for (const gap of gaps) {
|
|
252
|
+
const values = merged.get(gap.code) ?? [];
|
|
253
|
+
values.push(...gap.values);
|
|
254
|
+
merged.set(gap.code, values);
|
|
255
|
+
}
|
|
256
|
+
if (merged.size > MAX_GAPS)
|
|
257
|
+
throw new Error("native change proof exceeded the bounded gap taxonomy");
|
|
258
|
+
return sortCanonical([...merged].map(([code, values]) => {
|
|
259
|
+
const evidence = sortCanonical(values);
|
|
260
|
+
return { code, count: evidence.length, evidence_hash: changeProofHash(evidence) };
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Produce Hunch's standalone semantic proof for one exact committed tree transition.
|
|
265
|
+
*
|
|
266
|
+
* The proof is read-only and timestamp-free. It binds source bytes, parsed graph,
|
|
267
|
+
* current memory records and every explicit gap; it never grants execution, CI,
|
|
268
|
+
* deployment, merge, ranking, promotion or policy authority.
|
|
269
|
+
*/
|
|
270
|
+
export function deriveChangeProof(root, store, baseRef, resultRef = "HEAD", options = {}) {
|
|
271
|
+
const change = deriveChangeIdentity(root, baseRef, resultRef);
|
|
272
|
+
const changed = exactChangedPaths(root, change.base_revision, change.head_revision);
|
|
273
|
+
if (changed.paths.length + changed.gaps.reduce((sum, gap) => sum + gap.values.length, 0) !== change.file_count) {
|
|
274
|
+
throw new Error("native change proof path inventory does not match exact change identity");
|
|
275
|
+
}
|
|
276
|
+
const diff = exactDiff(root, change.base_revision, change.head_revision);
|
|
277
|
+
const baseScan = scanRepo(store, root, { churn: false, source: { kind: "commit", ref: change.base_revision } });
|
|
278
|
+
const resultScan = scanRepo(store, root, { churn: false, source: { kind: "commit", ref: change.head_revision } });
|
|
279
|
+
const dna = discoverProjectDna(root, change.head_revision);
|
|
280
|
+
const unknownEvidence = [...changed.gaps, ...diff.gaps];
|
|
281
|
+
for (const [label, scan] of [["base", baseScan], ["result", resultScan]]) {
|
|
282
|
+
const byCode = new Map();
|
|
283
|
+
for (const issue of scan.issues) {
|
|
284
|
+
const values = byCode.get(issue.code) ?? [];
|
|
285
|
+
values.push({ path: issue.path, detail_hash: changeProofHash(issue.detail) });
|
|
286
|
+
byCode.set(issue.code, values);
|
|
287
|
+
}
|
|
288
|
+
for (const [code, values] of byCode)
|
|
289
|
+
addGap(unknownEvidence, `${label}_graph_${code}`, values);
|
|
290
|
+
}
|
|
291
|
+
const blastSources = changed.paths.slice(0, MAX_CHANGED_FILES);
|
|
292
|
+
const rawBlast = mergeBlast(graphBlast(baseScan, blastSources, "base"), graphBlast(resultScan, blastSources, "result"));
|
|
293
|
+
const omissions = [];
|
|
294
|
+
if (changed.paths.length > MAX_CHANGED_FILES) {
|
|
295
|
+
addGap(omissions, "changed_files_omitted", changed.paths.slice(MAX_CHANGED_FILES));
|
|
296
|
+
}
|
|
297
|
+
if (rawBlast.length > MAX_INTERNAL_BLAST_RADIUS) {
|
|
298
|
+
addGap(unknownEvidence, "blast_radius_internal_cap", [{ observed: rawBlast.length, cap: MAX_INTERNAL_BLAST_RADIUS }]);
|
|
299
|
+
}
|
|
300
|
+
const boundedRawBlast = rawBlast.slice(0, MAX_INTERNAL_BLAST_RADIUS);
|
|
301
|
+
if (boundedRawBlast.length > MAX_BLAST_RADIUS) {
|
|
302
|
+
addGap(omissions, "blast_radius_omitted", boundedRawBlast.slice(MAX_BLAST_RADIUS));
|
|
303
|
+
}
|
|
304
|
+
const blast = boundedRawBlast.slice(0, MAX_BLAST_RADIUS);
|
|
305
|
+
const dependentPaths = sortedUnique(boundedRawBlast.map((entry) => entry.dependent_path));
|
|
306
|
+
const publicOnly = !!options.publicOnly;
|
|
307
|
+
const decisions = (publicOnly ? store.json.loadAll("decisions") : store.recs("decisions"));
|
|
308
|
+
const constraints = (publicOnly ? store.json.loadAll("constraints") : store.recs("constraints"));
|
|
309
|
+
const guardReport = store.buildCheckReport(changed.paths, diff.diff, {
|
|
310
|
+
strict: true,
|
|
311
|
+
publicOnly,
|
|
312
|
+
lastChange: (path) => lastChangeAt(root, change.head_revision, path),
|
|
313
|
+
});
|
|
314
|
+
const allStrictBlockerIds = sortedUnique([
|
|
315
|
+
...guardReport.direct.filter((item) => item.strictBlocks).map((item) => item.id),
|
|
316
|
+
...guardReport.regressions.filter((item) => item.blocking).map((item) => item.decision),
|
|
317
|
+
...guardReport.vetoes.filter((item) => item.blocking).map((item) => item.decision),
|
|
318
|
+
]);
|
|
319
|
+
const allRegressionDecisionIds = sortedUnique(guardReport.regressions.map((item) => item.decision));
|
|
320
|
+
const allVetoDecisionIds = sortedUnique(guardReport.vetoes.map((item) => item.decision));
|
|
321
|
+
if (allStrictBlockerIds.length > 2_048) {
|
|
322
|
+
addGap(omissions, "guard_blockers_omitted", allStrictBlockerIds.slice(2_048));
|
|
323
|
+
}
|
|
324
|
+
if (allRegressionDecisionIds.length > 1_024) {
|
|
325
|
+
addGap(omissions, "guard_regressions_omitted", allRegressionDecisionIds.slice(1_024));
|
|
326
|
+
}
|
|
327
|
+
if (allVetoDecisionIds.length > 1_024) {
|
|
328
|
+
addGap(omissions, "guard_vetoes_omitted", allVetoDecisionIds.slice(1_024));
|
|
329
|
+
}
|
|
330
|
+
const strictBlockerIds = allStrictBlockerIds.slice(0, 2_048);
|
|
331
|
+
const regressionDecisionIds = allRegressionDecisionIds.slice(0, 1_024);
|
|
332
|
+
const vetoDecisionIds = allVetoDecisionIds.slice(0, 1_024);
|
|
333
|
+
const guardDecisionIds = new Set([
|
|
334
|
+
...allRegressionDecisionIds,
|
|
335
|
+
...allVetoDecisionIds,
|
|
336
|
+
...guardReport.direct.flatMap((item) => item.why?.decision?.id ? [item.why.decision.id] : []),
|
|
337
|
+
]);
|
|
338
|
+
const stableGuardReport = {
|
|
339
|
+
direct: guardReport.direct.map((item) => ({
|
|
340
|
+
id: item.id,
|
|
341
|
+
files: [...item.files].sort(compareCodeUnits),
|
|
342
|
+
strict_blocks: item.strictBlocks,
|
|
343
|
+
downgrade: item.downgrade ?? null,
|
|
344
|
+
source_decision: item.why?.decision?.id ?? null,
|
|
345
|
+
})),
|
|
346
|
+
regressions: guardReport.regressions.map((item) => ({
|
|
347
|
+
decision: item.decision,
|
|
348
|
+
kind: item.kind,
|
|
349
|
+
name: item.name,
|
|
350
|
+
blocking: item.blocking,
|
|
351
|
+
})),
|
|
352
|
+
vetoes: guardReport.vetoes.map((item) => ({
|
|
353
|
+
decision: item.decision,
|
|
354
|
+
tier: item.tier,
|
|
355
|
+
blocking: item.blocking,
|
|
356
|
+
evidence_hash: changeProofHash(item.evidence),
|
|
357
|
+
})),
|
|
358
|
+
};
|
|
359
|
+
sortCanonical(stableGuardReport.direct);
|
|
360
|
+
sortCanonical(stableGuardReport.regressions);
|
|
361
|
+
sortCanonical(stableGuardReport.vetoes);
|
|
362
|
+
const conformanceResults = checkConformance(store, {
|
|
363
|
+
publicOnly,
|
|
364
|
+
graph: { symbols: resultScan.symbols, edges: resultScan.edges },
|
|
365
|
+
});
|
|
366
|
+
const conformancePredicates = decisions
|
|
367
|
+
.filter(isInForce)
|
|
368
|
+
.flatMap((decision) => (decision.conformance ?? []).map((predicate, predicateIndex) => ({
|
|
369
|
+
decision,
|
|
370
|
+
predicate,
|
|
371
|
+
predicateIndex,
|
|
372
|
+
})));
|
|
373
|
+
if (conformancePredicates.length !== conformanceResults.length) {
|
|
374
|
+
throw new Error("native change proof conformance inventory is inconsistent");
|
|
375
|
+
}
|
|
376
|
+
const allConformance = conformanceResults.map((result, index) => ({
|
|
377
|
+
decision_id: result.decision,
|
|
378
|
+
predicate_index: conformancePredicates[index].predicateIndex,
|
|
379
|
+
predicate_hash: changeProofHash(conformancePredicates[index].predicate),
|
|
380
|
+
satisfied: result.satisfied,
|
|
381
|
+
detail_hash: changeProofHash(result.detail),
|
|
382
|
+
}));
|
|
383
|
+
sortCanonical(allConformance);
|
|
384
|
+
let conformance = allConformance;
|
|
385
|
+
if (allConformance.length > MAX_CONFORMANCE) {
|
|
386
|
+
const failures = allConformance.filter((receipt) => !receipt.satisfied);
|
|
387
|
+
const passes = allConformance.filter((receipt) => receipt.satisfied);
|
|
388
|
+
conformance = sortCanonical([
|
|
389
|
+
...failures.slice(0, MAX_CONFORMANCE),
|
|
390
|
+
...passes.slice(0, Math.max(0, MAX_CONFORMANCE - failures.length)),
|
|
391
|
+
]);
|
|
392
|
+
const retained = new Set(conformance.map(canonicalChangeProofJson));
|
|
393
|
+
addGap(omissions, "conformance_receipts_omitted", allConformance.filter((receipt) => !retained.has(canonicalChangeProofJson(receipt))));
|
|
394
|
+
}
|
|
395
|
+
const conformanceDecisionIds = new Set(conformancePredicates.map(({ decision }) => decision.id));
|
|
396
|
+
const allDecisionRefs = decisions.filter(isInForce).flatMap((decision) => {
|
|
397
|
+
const changedPaths = changed.paths.filter((path) => decisionMatchesPath(decision, path, [baseScan, resultScan]));
|
|
398
|
+
const blastPaths = dependentPaths.filter((path) => decisionMatchesPath(decision, path, [baseScan, resultScan]));
|
|
399
|
+
const relevance = sortedUnique([
|
|
400
|
+
...(blastPaths.length ? ["blast_radius"] : []),
|
|
401
|
+
...(changedPaths.length ? ["changed_path"] : []),
|
|
402
|
+
...(conformanceDecisionIds.has(decision.id) ? ["conformance"] : []),
|
|
403
|
+
...(guardDecisionIds.has(decision.id) ? ["guard"] : []),
|
|
404
|
+
]);
|
|
405
|
+
if (!relevance.length)
|
|
406
|
+
return [];
|
|
407
|
+
const paths = sortedUnique([...changedPaths, ...blastPaths]);
|
|
408
|
+
if (paths.length > MAX_RECORD_PATHS)
|
|
409
|
+
addGap(omissions, "decision_paths_omitted", paths.slice(MAX_RECORD_PATHS));
|
|
410
|
+
return [{
|
|
411
|
+
id: decision.id,
|
|
412
|
+
record_hash: changeProofHash(decision),
|
|
413
|
+
relevance,
|
|
414
|
+
paths: paths.slice(0, MAX_RECORD_PATHS),
|
|
415
|
+
path_count: paths.length,
|
|
416
|
+
paths_hash: changeProofHash(paths),
|
|
417
|
+
}];
|
|
418
|
+
});
|
|
419
|
+
sortCanonical(allDecisionRefs);
|
|
420
|
+
if (allDecisionRefs.length > MAX_RECORDS)
|
|
421
|
+
addGap(omissions, "decision_refs_omitted", allDecisionRefs.slice(MAX_RECORDS));
|
|
422
|
+
const decisionRefs = allDecisionRefs.slice(0, MAX_RECORDS);
|
|
423
|
+
const allConstraintRefs = constraints.filter((constraint) => constraint.status !== "retired").flatMap((constraint) => {
|
|
424
|
+
const changedPaths = changed.paths.filter((path) => constraintMatchesPath(constraint, path));
|
|
425
|
+
const blastPaths = dependentPaths.filter((path) => constraintMatchesPath(constraint, path));
|
|
426
|
+
const relevance = sortedUnique([
|
|
427
|
+
...(blastPaths.length ? ["blast_radius"] : []),
|
|
428
|
+
...(changedPaths.length ? ["changed_path"] : []),
|
|
429
|
+
]);
|
|
430
|
+
if (!relevance.length)
|
|
431
|
+
return [];
|
|
432
|
+
const paths = sortedUnique([...changedPaths, ...blastPaths]);
|
|
433
|
+
if (paths.length > MAX_RECORD_PATHS)
|
|
434
|
+
addGap(omissions, "constraint_paths_omitted", paths.slice(MAX_RECORD_PATHS));
|
|
435
|
+
return [{
|
|
436
|
+
id: constraint.id,
|
|
437
|
+
record_hash: changeProofHash(constraint),
|
|
438
|
+
severity: constraint.severity,
|
|
439
|
+
relevance,
|
|
440
|
+
paths: paths.slice(0, MAX_RECORD_PATHS),
|
|
441
|
+
path_count: paths.length,
|
|
442
|
+
paths_hash: changeProofHash(paths),
|
|
443
|
+
}];
|
|
444
|
+
});
|
|
445
|
+
sortCanonical(allConstraintRefs);
|
|
446
|
+
if (allConstraintRefs.length > MAX_RECORDS)
|
|
447
|
+
addGap(omissions, "constraint_refs_omitted", allConstraintRefs.slice(MAX_RECORDS));
|
|
448
|
+
const constraintRefs = allConstraintRefs.slice(0, MAX_RECORDS);
|
|
449
|
+
const omissionReceipts = sealedGaps(omissions);
|
|
450
|
+
const unknownReceipts = sealedGaps(unknownEvidence);
|
|
451
|
+
const guard = {
|
|
452
|
+
verdict: strictBlockerIds.length ? "fail" : "pass",
|
|
453
|
+
strict_blocker_ids: strictBlockerIds,
|
|
454
|
+
regression_decision_ids: regressionDecisionIds,
|
|
455
|
+
veto_decision_ids: vetoDecisionIds,
|
|
456
|
+
report_hash: changeProofHash(stableGuardReport),
|
|
457
|
+
};
|
|
458
|
+
const verdict = guard.verdict === "fail" || allConformance.some((receipt) => !receipt.satisfied)
|
|
459
|
+
? "fail"
|
|
460
|
+
: omissionReceipts.length || unknownReceipts.length
|
|
461
|
+
? "unknown"
|
|
462
|
+
: "pass";
|
|
463
|
+
const unsigned = {
|
|
464
|
+
schema: CHANGE_PROOF_SCHEMA_VERSION,
|
|
465
|
+
algorithm: CHANGE_PROOF_ALGORITHM,
|
|
466
|
+
engine: { package: "@davesheffer/hunch", version: HUNCH_VERSION },
|
|
467
|
+
repository: {
|
|
468
|
+
repository_id: dna.repository_id,
|
|
469
|
+
base_revision: change.base_revision,
|
|
470
|
+
result_revision: change.head_revision,
|
|
471
|
+
},
|
|
472
|
+
change,
|
|
473
|
+
project_dna: {
|
|
474
|
+
schema: dna.schema,
|
|
475
|
+
profile_id: dna.profile_id,
|
|
476
|
+
repository_id: dna.repository_id,
|
|
477
|
+
repository_revision: dna.repository_revision,
|
|
478
|
+
content_hash: dna.content_hash,
|
|
479
|
+
trait_ids: dna.traits.map((trait) => trait.id).sort(compareCodeUnits),
|
|
480
|
+
},
|
|
481
|
+
graph: { base: graphSeal(baseScan), result: graphSeal(resultScan) },
|
|
482
|
+
changed_files: changed.paths.slice(0, MAX_CHANGED_FILES),
|
|
483
|
+
changed_file_count: change.file_count,
|
|
484
|
+
blast_radius: blast,
|
|
485
|
+
blast_radius_count: rawBlast.length,
|
|
486
|
+
decisions: decisionRefs,
|
|
487
|
+
decision_count: allDecisionRefs.length,
|
|
488
|
+
constraints: constraintRefs,
|
|
489
|
+
constraint_count: allConstraintRefs.length,
|
|
490
|
+
conformance,
|
|
491
|
+
conformance_count: allConformance.length,
|
|
492
|
+
guard,
|
|
493
|
+
memory: {
|
|
494
|
+
scope: publicOnly ? "public" : "union",
|
|
495
|
+
records_hash: changeProofHash({ decisions: decisionRefs, constraints: constraintRefs }),
|
|
496
|
+
},
|
|
497
|
+
omissions: omissionReceipts,
|
|
498
|
+
unknowns: unknownReceipts,
|
|
499
|
+
verdict,
|
|
500
|
+
authority: {
|
|
501
|
+
execution: false,
|
|
502
|
+
ci: false,
|
|
503
|
+
deployment: false,
|
|
504
|
+
merge: false,
|
|
505
|
+
ranking: false,
|
|
506
|
+
promotion: false,
|
|
507
|
+
policy: false,
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
return sealChangeProof(unsigned);
|
|
511
|
+
}
|
|
512
|
+
//# sourceMappingURL=changeProof.js.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CHANGE_PROOF_SCHEMA_VERSION: "hunch.change-proof/1";
|
|
3
|
+
export declare const CHANGE_PROOF_ALGORITHM: "hunch-change-proof-sha256/1";
|
|
4
|
+
export declare const CHANGE_PROOF_VERDICTS: readonly ["fail", "pass", "unknown"];
|
|
5
|
+
export declare const CHANGE_PROOF_RELEVANCE: readonly ["blast_radius", "changed_path", "conformance", "guard"];
|
|
6
|
+
export declare const ChangeProofSchema: z.ZodObject<{
|
|
7
|
+
schema: z.ZodLiteral<"hunch.change-proof/1">;
|
|
8
|
+
algorithm: z.ZodLiteral<"hunch-change-proof-sha256/1">;
|
|
9
|
+
proof_id: z.ZodString;
|
|
10
|
+
engine: z.ZodObject<{
|
|
11
|
+
package: z.ZodLiteral<"@davesheffer/hunch">;
|
|
12
|
+
version: z.ZodString;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
repository: z.ZodObject<{
|
|
15
|
+
repository_id: z.ZodString;
|
|
16
|
+
base_revision: z.ZodString;
|
|
17
|
+
result_revision: z.ZodString;
|
|
18
|
+
}, z.core.$strict>;
|
|
19
|
+
change: z.ZodObject<{
|
|
20
|
+
schema: z.ZodLiteral<"hunch.change-identity/1">;
|
|
21
|
+
algorithm: z.ZodLiteral<"git-raw-tree-delta-sha256/1">;
|
|
22
|
+
change_id: z.ZodString;
|
|
23
|
+
base_revision: z.ZodString;
|
|
24
|
+
head_revision: z.ZodString;
|
|
25
|
+
base_tree: z.ZodString;
|
|
26
|
+
head_tree: z.ZodString;
|
|
27
|
+
delta_hash: z.ZodString;
|
|
28
|
+
patch_id: z.ZodNullable<z.ZodString>;
|
|
29
|
+
file_count: z.ZodNumber;
|
|
30
|
+
paths_hash: z.ZodString;
|
|
31
|
+
content_hash: z.ZodString;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
project_dna: z.ZodObject<{
|
|
34
|
+
schema: z.ZodLiteral<"hunch.project-dna/1">;
|
|
35
|
+
profile_id: z.ZodString;
|
|
36
|
+
repository_id: z.ZodString;
|
|
37
|
+
repository_revision: z.ZodString;
|
|
38
|
+
content_hash: z.ZodString;
|
|
39
|
+
trait_ids: z.ZodArray<z.ZodString>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
graph: z.ZodObject<{
|
|
42
|
+
base: z.ZodObject<{
|
|
43
|
+
source: z.ZodLiteral<"commit">;
|
|
44
|
+
revision: z.ZodString;
|
|
45
|
+
source_hash: z.ZodString;
|
|
46
|
+
topology_hash: z.ZodString;
|
|
47
|
+
files: z.ZodNumber;
|
|
48
|
+
symbols: z.ZodNumber;
|
|
49
|
+
edges: z.ZodNumber;
|
|
50
|
+
components: z.ZodNumber;
|
|
51
|
+
issue_count: z.ZodNumber;
|
|
52
|
+
}, z.core.$strict>;
|
|
53
|
+
result: z.ZodObject<{
|
|
54
|
+
source: z.ZodLiteral<"commit">;
|
|
55
|
+
revision: z.ZodString;
|
|
56
|
+
source_hash: z.ZodString;
|
|
57
|
+
topology_hash: z.ZodString;
|
|
58
|
+
files: z.ZodNumber;
|
|
59
|
+
symbols: z.ZodNumber;
|
|
60
|
+
edges: z.ZodNumber;
|
|
61
|
+
components: z.ZodNumber;
|
|
62
|
+
issue_count: z.ZodNumber;
|
|
63
|
+
}, z.core.$strict>;
|
|
64
|
+
}, z.core.$strict>;
|
|
65
|
+
changed_files: z.ZodArray<z.ZodString>;
|
|
66
|
+
changed_file_count: z.ZodNumber;
|
|
67
|
+
blast_radius: z.ZodArray<z.ZodObject<{
|
|
68
|
+
source_path: z.ZodString;
|
|
69
|
+
dependent_path: z.ZodString;
|
|
70
|
+
depth: z.ZodNumber;
|
|
71
|
+
graphs: z.ZodArray<z.ZodEnum<{
|
|
72
|
+
base: "base";
|
|
73
|
+
result: "result";
|
|
74
|
+
}>>;
|
|
75
|
+
}, z.core.$strict>>;
|
|
76
|
+
blast_radius_count: z.ZodNumber;
|
|
77
|
+
decisions: z.ZodArray<z.ZodObject<{
|
|
78
|
+
id: z.ZodString;
|
|
79
|
+
record_hash: z.ZodString;
|
|
80
|
+
relevance: z.ZodArray<z.ZodEnum<{
|
|
81
|
+
blast_radius: "blast_radius";
|
|
82
|
+
changed_path: "changed_path";
|
|
83
|
+
conformance: "conformance";
|
|
84
|
+
guard: "guard";
|
|
85
|
+
}>>;
|
|
86
|
+
paths: z.ZodArray<z.ZodString>;
|
|
87
|
+
path_count: z.ZodNumber;
|
|
88
|
+
paths_hash: z.ZodString;
|
|
89
|
+
}, z.core.$strict>>;
|
|
90
|
+
decision_count: z.ZodNumber;
|
|
91
|
+
constraints: z.ZodArray<z.ZodObject<{
|
|
92
|
+
id: z.ZodString;
|
|
93
|
+
record_hash: z.ZodString;
|
|
94
|
+
severity: z.ZodEnum<{
|
|
95
|
+
advisory: "advisory";
|
|
96
|
+
warning: "warning";
|
|
97
|
+
blocking: "blocking";
|
|
98
|
+
}>;
|
|
99
|
+
relevance: z.ZodArray<z.ZodEnum<{
|
|
100
|
+
blast_radius: "blast_radius";
|
|
101
|
+
changed_path: "changed_path";
|
|
102
|
+
}>>;
|
|
103
|
+
paths: z.ZodArray<z.ZodString>;
|
|
104
|
+
path_count: z.ZodNumber;
|
|
105
|
+
paths_hash: z.ZodString;
|
|
106
|
+
}, z.core.$strict>>;
|
|
107
|
+
constraint_count: z.ZodNumber;
|
|
108
|
+
conformance: z.ZodArray<z.ZodObject<{
|
|
109
|
+
decision_id: z.ZodString;
|
|
110
|
+
predicate_index: z.ZodNumber;
|
|
111
|
+
predicate_hash: z.ZodString;
|
|
112
|
+
satisfied: z.ZodBoolean;
|
|
113
|
+
detail_hash: z.ZodString;
|
|
114
|
+
}, z.core.$strict>>;
|
|
115
|
+
conformance_count: z.ZodNumber;
|
|
116
|
+
guard: z.ZodObject<{
|
|
117
|
+
verdict: z.ZodEnum<{
|
|
118
|
+
fail: "fail";
|
|
119
|
+
pass: "pass";
|
|
120
|
+
}>;
|
|
121
|
+
strict_blocker_ids: z.ZodArray<z.ZodString>;
|
|
122
|
+
regression_decision_ids: z.ZodArray<z.ZodString>;
|
|
123
|
+
veto_decision_ids: z.ZodArray<z.ZodString>;
|
|
124
|
+
report_hash: z.ZodString;
|
|
125
|
+
}, z.core.$strict>;
|
|
126
|
+
memory: z.ZodObject<{
|
|
127
|
+
scope: z.ZodEnum<{
|
|
128
|
+
union: "union";
|
|
129
|
+
public: "public";
|
|
130
|
+
}>;
|
|
131
|
+
records_hash: z.ZodString;
|
|
132
|
+
}, z.core.$strict>;
|
|
133
|
+
omissions: z.ZodArray<z.ZodObject<{
|
|
134
|
+
code: z.ZodString;
|
|
135
|
+
count: z.ZodNumber;
|
|
136
|
+
evidence_hash: z.ZodString;
|
|
137
|
+
}, z.core.$strict>>;
|
|
138
|
+
unknowns: z.ZodArray<z.ZodObject<{
|
|
139
|
+
code: z.ZodString;
|
|
140
|
+
count: z.ZodNumber;
|
|
141
|
+
evidence_hash: z.ZodString;
|
|
142
|
+
}, z.core.$strict>>;
|
|
143
|
+
verdict: z.ZodEnum<{
|
|
144
|
+
fail: "fail";
|
|
145
|
+
pass: "pass";
|
|
146
|
+
unknown: "unknown";
|
|
147
|
+
}>;
|
|
148
|
+
authority: z.ZodObject<{
|
|
149
|
+
execution: z.ZodLiteral<false>;
|
|
150
|
+
ci: z.ZodLiteral<false>;
|
|
151
|
+
deployment: z.ZodLiteral<false>;
|
|
152
|
+
merge: z.ZodLiteral<false>;
|
|
153
|
+
ranking: z.ZodLiteral<false>;
|
|
154
|
+
promotion: z.ZodLiteral<false>;
|
|
155
|
+
policy: z.ZodLiteral<false>;
|
|
156
|
+
}, z.core.$strict>;
|
|
157
|
+
content_hash: z.ZodString;
|
|
158
|
+
}, z.core.$strict>;
|
|
159
|
+
export type ChangeProof = z.infer<typeof ChangeProofSchema>;
|
|
160
|
+
export type ChangeProofUnsigned = Omit<ChangeProof, "proof_id" | "content_hash">;
|
|
161
|
+
export declare function canonicalChangeProofJson(value: unknown): string;
|
|
162
|
+
export declare function changeProofHash(value: unknown): string;
|
|
163
|
+
export declare function sealChangeProof(unsigned: ChangeProofUnsigned): ChangeProof;
|
|
164
|
+
export declare function assertChangeProof(value: unknown): asserts value is ChangeProof;
|