@davesheffer/hunch 1.8.3 → 1.9.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/README.md +92 -1
- package/dist/cli/index.js +1222 -397
- package/dist/constitution/adapters.js +31 -14
- package/dist/constitution/behaviorEvaluator.js +20 -7
- package/dist/constitution/behaviorProof.js +3 -2
- package/dist/constitution/canonical.js +7 -1
- package/dist/constitution/card.js +7 -2
- package/dist/constitution/compiler.js +71 -1
- package/dist/constitution/correctionPolicyMaterializer.js +496 -0
- package/dist/constitution/delta.js +3 -2
- package/dist/constitution/evaluator.js +29 -3
- package/dist/constitution/experiment.js +6 -5
- package/dist/constitution/experimentRunner.js +43 -14
- package/dist/constitution/g2BehaviorCandidates.js +49 -26
- package/dist/constitution/g2BehaviorDependencies.js +203 -14
- package/dist/constitution/g2Candidates.js +1 -1
- package/dist/constitution/lifecycle.js +17 -0
- package/dist/constitution/plan.js +26 -9
- package/dist/constitution/replacementFreeGit.js +67 -0
- package/dist/constitution/replay.js +6 -0
- package/dist/constitution/replayCache.js +1 -1
- package/dist/constitution/replayWorker.js +1 -1
- package/dist/constitution/repository.js +141 -5
- package/dist/constitution/safeCheckout.js +75 -0
- package/dist/constitution/schema.js +30 -5
- package/dist/constitution/service.js +53 -10
- package/dist/constitution/sourceMutation.js +65 -12
- package/dist/constitution/staticGraphBaseline.js +44 -0
- package/dist/constitution/structural.js +60 -4
- package/dist/core/autoreview.js +1 -1
- package/dist/core/canonicalOrder.js +6 -0
- package/dist/core/conformance.js +68 -27
- package/dist/core/docscan.js +2 -1
- package/dist/core/escalations.js +11 -0
- package/dist/core/io.js +44 -9
- package/dist/core/overlaySafety.js +178 -0
- package/dist/core/paths.js +13 -2
- package/dist/core/safeRepoFile.js +74 -0
- package/dist/extractors/comments.js +6 -8
- package/dist/extractors/git.js +1631 -82
- package/dist/extractors/indexer.js +86 -47
- package/dist/extractors/repoSource.js +390 -0
- package/dist/integrations/ciAction.js +10 -2
- package/dist/integrations/gitignore.js +44 -5
- package/dist/integrations/mergeDriver.js +23 -5
- package/dist/integrations/sync.js +61 -5
- package/dist/integrations/team.js +666 -23
- package/dist/mcp/server.js +261 -34
- package/dist/store/db.js +57 -7
- package/dist/store/hunchStore.js +92 -11
- package/dist/store/jsonStore.js +350 -63
- package/dist/store/schema.js +27 -11
- package/dist/synthesis/provider.js +13 -4
- package/dist/synthesis/synthesize.js +56 -19
- package/dist/wiki/graph.js +5 -4
- package/dist/wiki/wiki.js +16 -10
- package/package.json +7 -2
- package/tooling/md1-benchmark.mjs +628 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync,
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { canonicalHash } from "./canonical.js";
|
|
8
|
+
import { hasUnsafeCheckoutAttributes } from "./safeCheckout.js";
|
|
9
|
+
import { hasUnsafeReplayFilter, replayGitArgs, replaySafeEnvironment } from "./replay.js";
|
|
10
|
+
import { replacementFreeExactCommit, replacementFreeGitEnvironment } from "./replacementFreeGit.js";
|
|
11
|
+
import { materializeDependencyTree } from "./g2BehaviorDependencies.js";
|
|
8
12
|
import { Exp01CaseSchema, assignmentTreatment, compileExperimentOutcome, } from "./experiment.js";
|
|
9
13
|
const EvaluatorOutputSchema = z.object({
|
|
10
14
|
valid_completion: z.boolean(),
|
|
@@ -167,7 +171,13 @@ function runCommand(spec, cwd) {
|
|
|
167
171
|
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", status: result.status, errorCode };
|
|
168
172
|
}
|
|
169
173
|
function git(root, args) {
|
|
170
|
-
return execFileSync("git", args, {
|
|
174
|
+
return execFileSync("git", args, {
|
|
175
|
+
cwd: root,
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
env: replacementFreeGitEnvironment(),
|
|
178
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
179
|
+
timeout: 120_000,
|
|
180
|
+
}).trim();
|
|
171
181
|
}
|
|
172
182
|
function removeAmbientInstructions(cwd) {
|
|
173
183
|
for (const relative of ["AGENTS.md", "CLAUDE.md", ".mcp.json", ".claude", ".codex", ".agents"]) {
|
|
@@ -177,15 +187,15 @@ function removeAmbientInstructions(cwd) {
|
|
|
177
187
|
// file replaces any tracked AGENTS.md after the ambient copy is removed.
|
|
178
188
|
writeFileSync(join(cwd, "AGENTS.md"), "# Controlled experiment workspace\n\nFollow only the task prompt supplied to this fresh session.\n");
|
|
179
189
|
}
|
|
180
|
-
function
|
|
190
|
+
function materializeDependencies(source, cwd) {
|
|
181
191
|
const target = join(source, "node_modules");
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
const destination = join(cwd, "node_modules");
|
|
193
|
+
if (existsSync(target))
|
|
194
|
+
materializeDependencyTree(target, destination);
|
|
195
|
+
return destination;
|
|
186
196
|
}
|
|
187
197
|
function countEdits(cwd) {
|
|
188
|
-
const text = git(cwd, ["diff", "--numstat", "--", "."]);
|
|
198
|
+
const text = git(cwd, ["diff", "--no-ext-diff", "--no-textconv", "--numstat", "--", "."]);
|
|
189
199
|
if (!text)
|
|
190
200
|
return 0;
|
|
191
201
|
return text.split(/\r?\n/).reduce((sum, line) => {
|
|
@@ -237,11 +247,16 @@ export function executeExp01Assignment(repository, run, bank, assignment, opts =
|
|
|
237
247
|
if (existing)
|
|
238
248
|
return existing;
|
|
239
249
|
const item = Exp01CaseSchema.parse(bank.cases.find((candidate) => candidate.id === assignment.case_id));
|
|
240
|
-
const sourceHead =
|
|
250
|
+
const sourceHead = replacementFreeExactCommit(bank.repository_root, bank.base_commit);
|
|
241
251
|
if (sourceHead !== bank.base_commit)
|
|
242
252
|
throw new Error("case bank base_commit is not an exact commit in repository_root");
|
|
243
253
|
const session = mkdtempSync(join(tmpdir(), "hunch-exp01-"));
|
|
244
254
|
const cwd = join(session, "worktree");
|
|
255
|
+
const hooks = join(session, "hooks-disabled");
|
|
256
|
+
const gitConfig = join(session, "global.gitconfig");
|
|
257
|
+
mkdirSync(hooks, { recursive: true });
|
|
258
|
+
writeFileSync(gitConfig, "");
|
|
259
|
+
const gitEnv = replaySafeEnvironment(session, gitConfig);
|
|
245
260
|
let added = false;
|
|
246
261
|
let invocationStarted = false;
|
|
247
262
|
try {
|
|
@@ -249,9 +264,19 @@ export function executeExp01Assignment(repository, run, bank, assignment, opts =
|
|
|
249
264
|
if (currentProviderVersion !== run.runner.provider_version) {
|
|
250
265
|
return failureOutcome(repository, run, assignment, "infrastructure_failure", false, "Selected subscription CLI version drifted after assignment; assignment was excluded before model invocation.", "provider-version-drift", { evaluator: canonicalHash({ expected: run.runner.provider_version, actual: currentProviderVersion }) }, opts.now);
|
|
251
266
|
}
|
|
252
|
-
|
|
267
|
+
if (hasUnsafeReplayFilter(bank.repository_root, gitEnv)) {
|
|
268
|
+
return failureOutcome(repository, run, assignment, "infrastructure_failure", false, "Repository-local Git content filters make the controlled checkout unsafe.", "unsafe-local-filter-config", {}, opts.now);
|
|
269
|
+
}
|
|
270
|
+
if (hasUnsafeCheckoutAttributes(bank.repository_root, bank.base_commit, gitEnv, { allowDisabledLfs: true })) {
|
|
271
|
+
return failureOutcome(repository, run, assignment, "infrastructure_failure", false, "Git checkout attributes could transform or execute content outside the locked case bytes.", "unsafe-checkout-attributes", {}, opts.now);
|
|
272
|
+
}
|
|
273
|
+
execFileSync("git", replayGitArgs(bank.repository_root, hooks, ["worktree", "add", "--detach", "--force", cwd, bank.base_commit]), {
|
|
274
|
+
env: gitEnv,
|
|
275
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
276
|
+
timeout: 120_000,
|
|
277
|
+
});
|
|
253
278
|
added = true;
|
|
254
|
-
|
|
279
|
+
const dependencyRoot = materializeDependencies(bank.repository_root, cwd);
|
|
255
280
|
removeAmbientInstructions(cwd);
|
|
256
281
|
if (item.setup) {
|
|
257
282
|
if (!isAbsolute(item.setup.artifact) || !existsSync(item.setup.artifact) || externalArtifactHash(item.setup.artifact) !== item.setup.artifact_hash) {
|
|
@@ -271,9 +296,9 @@ export function executeExp01Assignment(repository, run, bank, assignment, opts =
|
|
|
271
296
|
}
|
|
272
297
|
const treatment = assignmentTreatment(bank, run, assignment);
|
|
273
298
|
invocationStarted = true;
|
|
274
|
-
const agent = invokeAgent(run, cwd, controlledPrompt(treatment), opts.timeoutMs ?? 30 * 60 * 1000,
|
|
299
|
+
const agent = invokeAgent(run, cwd, controlledPrompt(treatment), opts.timeoutMs ?? 30 * 60 * 1000, dependencyRoot);
|
|
275
300
|
const outputHash = canonicalHash(agent.stdout);
|
|
276
|
-
const diff = git(cwd, ["diff", "--binary", "--", "."]);
|
|
301
|
+
const diff = git(cwd, ["diff", "--no-ext-diff", "--no-textconv", "--binary", "--", "."]);
|
|
277
302
|
const diffHash = canonicalHash(diff);
|
|
278
303
|
if (agent.errorCode) {
|
|
279
304
|
return failureOutcome(repository, run, assignment, "invalid_completion", true, "Subscription CLI did not produce a successful terminal run; outcome remains visible and unscored.", agent.errorCode, { output: outputHash, diff: diffHash }, opts.now);
|
|
@@ -333,7 +358,11 @@ export function executeExp01Assignment(repository, run, bank, assignment, opts =
|
|
|
333
358
|
finally {
|
|
334
359
|
try {
|
|
335
360
|
if (added)
|
|
336
|
-
execFileSync("git", ["worktree", "remove", "--force", cwd], {
|
|
361
|
+
execFileSync("git", replayGitArgs(bank.repository_root, hooks, ["worktree", "remove", "--force", cwd]), {
|
|
362
|
+
env: gitEnv,
|
|
363
|
+
stdio: "ignore",
|
|
364
|
+
timeout: 120_000,
|
|
365
|
+
});
|
|
337
366
|
}
|
|
338
367
|
catch {
|
|
339
368
|
// The assignment outcome already records any execution failure; prune is best effort.
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync,
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { shortHash } from "../core/ids.js";
|
|
7
|
+
import { compareCodeUnits } from "../core/canonicalOrder.js";
|
|
7
8
|
import { loadNativeTreeSitter } from "../extractors/nativeTreeSitter.js";
|
|
8
|
-
import { commitMeta, revExists } from "../extractors/git.js";
|
|
9
9
|
import { canonicalHash } from "./canonical.js";
|
|
10
10
|
import { buildG2CandidateReview, positiveBound, } from "./g2Candidates.js";
|
|
11
11
|
import { cleanupReplayWorktree, hasUnsafeReplayFilter, replayGitArgs, replaySafeEnvironment, } from "./replay.js";
|
|
12
|
-
import { dependencySnapshotForCommit } from "./g2BehaviorDependencies.js";
|
|
12
|
+
import { dependencySnapshotForCommit, materializeDependencySnapshot } from "./g2BehaviorDependencies.js";
|
|
13
|
+
import { hasUnsafeCheckoutAttributes } from "./safeCheckout.js";
|
|
14
|
+
import { replacementFreeCommitFiles, replacementFreeExactCommit, replacementFreeGitEnvironment, } from "./replacementFreeGit.js";
|
|
13
15
|
import { NODE_TEST_REPORTER_SOURCE, exactNodeTestPattern, nodeTestIsolationFlag, nodeTestReporterEvents, } from "./nodeTestEvidence.js";
|
|
14
16
|
const TEST_FILE = /\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/;
|
|
15
17
|
const FULL_SHA = /^[a-f0-9]{40}$/;
|
|
@@ -38,6 +40,7 @@ function fileAt(root, commit, file) {
|
|
|
38
40
|
try {
|
|
39
41
|
return execFileSync("git", ["-C", root, "show", `${commit}:${file}`], {
|
|
40
42
|
encoding: "utf8",
|
|
43
|
+
env: replacementFreeGitEnvironment(),
|
|
41
44
|
maxBuffer: 10 * 1024 * 1024,
|
|
42
45
|
stdio: ["ignore", "pipe", "ignore"],
|
|
43
46
|
});
|
|
@@ -203,8 +206,9 @@ function literalNodeTestCases(file, source) {
|
|
|
203
206
|
function addedLineNumbers(root, knownBad, knownGood, file) {
|
|
204
207
|
let diff;
|
|
205
208
|
try {
|
|
206
|
-
diff = execFileSync("git", ["-C", root, "diff", "--unified=0", "--no-ext-diff", knownBad, knownGood, "--", file], {
|
|
209
|
+
diff = execFileSync("git", ["-C", root, "diff", "--unified=0", "--no-ext-diff", "--no-textconv", knownBad, knownGood, "--", file], {
|
|
207
210
|
encoding: "utf8",
|
|
211
|
+
env: replacementFreeGitEnvironment(),
|
|
208
212
|
maxBuffer: 10 * 1024 * 1024,
|
|
209
213
|
stdio: ["ignore", "pipe", "ignore"],
|
|
210
214
|
});
|
|
@@ -271,13 +275,28 @@ function currentHumanDecision(store, decisionId) {
|
|
|
271
275
|
}
|
|
272
276
|
function directDecisionCommit(store, root, decisionId) {
|
|
273
277
|
const decision = currentHumanDecision(store, decisionId);
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
278
|
+
const sha = replacementFreeExactCommit(root, decision.commit);
|
|
279
|
+
if (!sha || !FULL_SHA.test(sha))
|
|
276
280
|
throw new Error(`G2 behavior decision ${decisionId} fixing commit is unavailable`);
|
|
281
|
+
let subject;
|
|
282
|
+
let date;
|
|
283
|
+
try {
|
|
284
|
+
const raw = execFileSync("git", ["-C", root, "show", "-s", "--format=%s%x00%aI", sha], {
|
|
285
|
+
encoding: "utf8",
|
|
286
|
+
env: replacementFreeGitEnvironment(),
|
|
287
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
288
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
289
|
+
}).trimEnd();
|
|
290
|
+
[subject = "", date = ""] = raw.split("\0");
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
throw new Error(`G2 behavior decision ${decisionId} fixing commit metadata is unavailable`);
|
|
294
|
+
}
|
|
277
295
|
let knownBad;
|
|
278
296
|
try {
|
|
279
|
-
knownBad = execFileSync("git", ["-C", root, "rev-parse", `${
|
|
297
|
+
knownBad = execFileSync("git", ["-C", root, "rev-parse", `${sha}^`], {
|
|
280
298
|
encoding: "utf8",
|
|
299
|
+
env: replacementFreeGitEnvironment(),
|
|
281
300
|
stdio: ["ignore", "pipe", "ignore"],
|
|
282
301
|
}).trim();
|
|
283
302
|
}
|
|
@@ -287,10 +306,10 @@ function directDecisionCommit(store, root, decisionId) {
|
|
|
287
306
|
if (!FULL_SHA.test(knownBad))
|
|
288
307
|
throw new Error(`G2 behavior decision ${decisionId} first parent is invalid`);
|
|
289
308
|
return {
|
|
290
|
-
commit:
|
|
291
|
-
subject
|
|
292
|
-
date
|
|
293
|
-
changedFiles: new Set(
|
|
309
|
+
commit: sha,
|
|
310
|
+
subject,
|
|
311
|
+
date,
|
|
312
|
+
changedFiles: new Set(replacementFreeCommitFiles(root, sha)),
|
|
294
313
|
decisionId: decision.id,
|
|
295
314
|
knownBad,
|
|
296
315
|
groundingHash: canonicalHash(decision),
|
|
@@ -360,7 +379,7 @@ function directDecisionReview(store, root, opts, behaviorResolutions, since, max
|
|
|
360
379
|
if (resolution)
|
|
361
380
|
candidate.human_review = resolution;
|
|
362
381
|
}
|
|
363
|
-
candidates.sort((left, right) => left.test.file
|
|
382
|
+
candidates.sort((left, right) => compareCodeUnits(left.test.file, right.test.file) || compareCodeUnits(left.test.name, right.test.name));
|
|
364
383
|
const items = candidates.slice(0, limit);
|
|
365
384
|
const reviewCounts = candidates.some((candidate) => candidate.human_review !== undefined) ? {
|
|
366
385
|
selected_candidates: candidates.filter((candidate) => candidate.human_review?.disposition === "selected").length,
|
|
@@ -425,9 +444,9 @@ export function buildG2BehaviorCandidateReview(store, root, opts = {}, resolutio
|
|
|
425
444
|
const candidates = [];
|
|
426
445
|
const failures = [];
|
|
427
446
|
const withoutTests = [];
|
|
428
|
-
for (const commit of [...commits.values()].sort((left, right) => left.commit
|
|
447
|
+
for (const commit of [...commits.values()].sort((left, right) => compareCodeUnits(left.commit, right.commit))) {
|
|
429
448
|
let found = false;
|
|
430
|
-
for (const file of [...commit.changedFiles].filter(safeTestFile).sort()) {
|
|
449
|
+
for (const file of [...commit.changedFiles].filter(safeTestFile).sort(compareCodeUnits)) {
|
|
431
450
|
const after = fileAt(root, commit.commit, file);
|
|
432
451
|
if (after == null) {
|
|
433
452
|
failures.push({ commit: commit.commit, file, error: "known-good test source unavailable" });
|
|
@@ -447,9 +466,9 @@ export function buildG2BehaviorCandidateReview(store, root, opts = {}, resolutio
|
|
|
447
466
|
statement: name,
|
|
448
467
|
test: { file, name, source_hash: sourceHash },
|
|
449
468
|
runner: runnerFor(file, name),
|
|
450
|
-
decision_ids: [...commit.decisionIds].sort(),
|
|
451
|
-
source_candidate_ids: [...commit.sourceCandidateIds].sort(),
|
|
452
|
-
source_attestation_ids: [...commit.sourceAttestationIds].sort(),
|
|
469
|
+
decision_ids: [...commit.decisionIds].sort(compareCodeUnits),
|
|
470
|
+
source_candidate_ids: [...commit.sourceCandidateIds].sort(compareCodeUnits),
|
|
471
|
+
source_attestation_ids: [...commit.sourceAttestationIds].sort(compareCodeUnits),
|
|
453
472
|
proposed_corpus: {
|
|
454
473
|
known_bad: { ref: commit.knownBad, expected: "failed" },
|
|
455
474
|
known_good: { ref: commit.commit, expected: "passed" },
|
|
@@ -471,10 +490,10 @@ export function buildG2BehaviorCandidateReview(store, root, opts = {}, resolutio
|
|
|
471
490
|
if (resolution)
|
|
472
491
|
candidate.human_review = resolution;
|
|
473
492
|
}
|
|
474
|
-
candidates.sort((left, right) => right.commit_date
|
|
475
|
-
|| left.commit
|
|
476
|
-
|| left.test.file
|
|
477
|
-
|| left.test.name
|
|
493
|
+
candidates.sort((left, right) => compareCodeUnits(right.commit_date, left.commit_date)
|
|
494
|
+
|| compareCodeUnits(left.commit, right.commit)
|
|
495
|
+
|| compareCodeUnits(left.test.file, right.test.file)
|
|
496
|
+
|| compareCodeUnits(left.test.name, right.test.name));
|
|
478
497
|
const items = candidates.slice(0, limit);
|
|
479
498
|
const reviewCounts = candidates.some((candidate) => candidate.human_review !== undefined) ? {
|
|
480
499
|
selected_candidates: candidates.filter((candidate) => candidate.human_review?.disposition === "selected").length,
|
|
@@ -490,8 +509,8 @@ export function buildG2BehaviorCandidateReview(store, root, opts = {}, resolutio
|
|
|
490
509
|
candidate_commits: commits.size,
|
|
491
510
|
behavior_candidates: candidates.length,
|
|
492
511
|
...reviewCounts,
|
|
493
|
-
commits_without_added_tests: withoutTests.sort(),
|
|
494
|
-
extraction_failures: failures.sort((left, right) => left.commit
|
|
512
|
+
commits_without_added_tests: withoutTests.sort(compareCodeUnits),
|
|
513
|
+
extraction_failures: failures.sort((left, right) => compareCodeUnits(left.commit, right.commit) || compareCodeUnits(left.file, right.file)),
|
|
495
514
|
items,
|
|
496
515
|
has_more: candidates.length > items.length,
|
|
497
516
|
limitations: LIMITATIONS,
|
|
@@ -523,12 +542,16 @@ export function nodeTestInfrastructureError(output) {
|
|
|
523
542
|
return null;
|
|
524
543
|
}
|
|
525
544
|
function runLeg(root, session, hooks, env, candidate, commit, expected, source, budgetMs) {
|
|
526
|
-
if (!FULL_SHA.test(commit) ||
|
|
545
|
+
if (!FULL_SHA.test(commit) || replacementFreeExactCommit(root, commit) !== commit)
|
|
527
546
|
return errorLeg(commit, expected, "commit-ref-unresolved");
|
|
528
547
|
const run = mkdtempSync(join(session, `${expected}-`));
|
|
529
548
|
const checkout = join(run, "checkout");
|
|
530
549
|
const dependencySnapshot = dependencySnapshotForCommit(root, commit);
|
|
531
550
|
const dependencySnapshotId = dependencySnapshot?.snapshot.id;
|
|
551
|
+
if (hasUnsafeCheckoutAttributes(root, commit, env, { allowDisabledLfs: true })) {
|
|
552
|
+
rmSync(run, { recursive: true, force: true });
|
|
553
|
+
return errorLeg(commit, expected, "unsafe-checkout-attributes", dependencySnapshotId);
|
|
554
|
+
}
|
|
532
555
|
let added = false;
|
|
533
556
|
let leg;
|
|
534
557
|
try {
|
|
@@ -539,7 +562,7 @@ function runLeg(root, session, hooks, env, candidate, commit, expected, source,
|
|
|
539
562
|
});
|
|
540
563
|
added = true;
|
|
541
564
|
if (dependencySnapshot) {
|
|
542
|
-
|
|
565
|
+
materializeDependencySnapshot(dependencySnapshot, join(checkout, "node_modules"));
|
|
543
566
|
}
|
|
544
567
|
const testFile = join(checkout, candidate.test.file);
|
|
545
568
|
mkdirSync(dirname(testFile), { recursive: true });
|
|
@@ -565,7 +588,7 @@ function runLeg(root, session, hooks, env, candidate, commit, expected, source,
|
|
|
565
588
|
args = testArgs;
|
|
566
589
|
}
|
|
567
590
|
else {
|
|
568
|
-
const tsx = dependencySnapshot ? join(
|
|
591
|
+
const tsx = dependencySnapshot ? join(checkout, "node_modules", "tsx", "dist", "cli.mjs") : "";
|
|
569
592
|
args = existsSync(tsx) ? [tsx, ...testArgs] : null;
|
|
570
593
|
}
|
|
571
594
|
if (!args) {
|
|
@@ -1,22 +1,207 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
3
|
+
import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readlinkSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
import { TextDecoder } from "node:util";
|
|
5
6
|
import { shortHash } from "../core/ids.js";
|
|
7
|
+
import { compareCodeUnits } from "../core/canonicalOrder.js";
|
|
6
8
|
import { canonicalHash, canonicalJson } from "./canonical.js";
|
|
7
9
|
import { replaySafeEnvironment } from "./replay.js";
|
|
10
|
+
import { replacementFreeExactCommit, replacementFreeGitEnvironment } from "./replacementFreeGit.js";
|
|
8
11
|
const FULL_SHA = /^[a-f0-9]{40}$/;
|
|
9
12
|
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
10
|
-
|
|
13
|
+
// v1 bound only the installed lock and native binaries. Including the version
|
|
14
|
+
// in SnapshotInput makes those caches ineligible and provisions a fresh v2 tree.
|
|
15
|
+
const SNAPSHOT_VERSION = 2;
|
|
16
|
+
const TREE_HASH_VERSION = "hunch-node-modules-tree-v1";
|
|
17
|
+
const HASH_BUFFER_BYTES = 64 * 1024;
|
|
18
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
11
19
|
function sha256(value) {
|
|
12
20
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
13
21
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
function fileSha256(file) {
|
|
23
|
+
const hash = createHash("sha256");
|
|
24
|
+
const buffer = Buffer.allocUnsafe(HASH_BUFFER_BYTES);
|
|
25
|
+
const fd = openSync(file, "r");
|
|
26
|
+
try {
|
|
27
|
+
for (let bytes = readSync(fd, buffer, 0, buffer.length, null); bytes > 0; bytes = readSync(fd, buffer, 0, buffer.length, null)) {
|
|
28
|
+
hash.update(buffer.subarray(0, bytes));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
return `sha256:${hash.digest("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
function hashField(hash, value) {
|
|
37
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : value;
|
|
38
|
+
const length = Buffer.allocUnsafe(8);
|
|
39
|
+
length.writeBigUInt64BE(BigInt(bytes.length));
|
|
40
|
+
hash.update(length);
|
|
41
|
+
hash.update(bytes);
|
|
42
|
+
}
|
|
43
|
+
function safeEntryName(name) {
|
|
44
|
+
if (!name || name === "." || name === ".." || name.includes("\0") || name.includes("/") || (sep === "\\" && name.includes("\\"))) {
|
|
45
|
+
throw new Error("dependency snapshot contains an unsafe filesystem entry name");
|
|
46
|
+
}
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
function symlinkTarget(link) {
|
|
50
|
+
const raw = readlinkSync(link, { encoding: "buffer" });
|
|
51
|
+
try {
|
|
52
|
+
return UTF8_DECODER.decode(raw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error("dependency snapshot contains a non-UTF-8 symlink target");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function inside(root, target) {
|
|
59
|
+
return target === root || target.startsWith(`${root}${sep}`);
|
|
60
|
+
}
|
|
61
|
+
function safeInternalSymlink(root, link, target) {
|
|
62
|
+
if (!target || target.includes("\0") || isAbsolute(target)) {
|
|
63
|
+
throw new Error("dependency snapshot contains an absolute or empty symlink target");
|
|
64
|
+
}
|
|
65
|
+
const lexicalRoot = resolve(root);
|
|
66
|
+
const lexicalTarget = resolve(dirname(link), target);
|
|
67
|
+
if (!inside(lexicalRoot, lexicalTarget)) {
|
|
68
|
+
throw new Error("dependency snapshot contains an escaping symlink target");
|
|
69
|
+
}
|
|
70
|
+
const realRoot = realpathSync(root);
|
|
71
|
+
const realTarget = realpathSync(lexicalTarget);
|
|
72
|
+
if (!inside(realRoot, realTarget)) {
|
|
73
|
+
throw new Error("dependency snapshot contains a symlink target outside node_modules");
|
|
74
|
+
}
|
|
75
|
+
const targetStat = statSync(link);
|
|
76
|
+
if (targetStat.isFile())
|
|
77
|
+
return "file";
|
|
78
|
+
if (targetStat.isDirectory())
|
|
79
|
+
return "dir";
|
|
80
|
+
throw new Error("dependency snapshot symlink resolves to a special filesystem entry");
|
|
81
|
+
}
|
|
82
|
+
function treeEntries(dir) {
|
|
83
|
+
return readdirSync(dir, { encoding: "buffer" })
|
|
84
|
+
.sort((left, right) => Buffer.compare(left, right))
|
|
85
|
+
.map((raw) => {
|
|
86
|
+
let name;
|
|
87
|
+
try {
|
|
88
|
+
name = UTF8_DECODER.decode(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error("dependency snapshot contains a non-UTF-8 filesystem entry name");
|
|
92
|
+
}
|
|
93
|
+
return safeEntryName(name);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function treeRecord(hash, relative, kind, mode, payload = "") {
|
|
97
|
+
hashField(hash, relative);
|
|
98
|
+
hashField(hash, kind);
|
|
99
|
+
hashField(hash, (mode & 0o7777).toString(8).padStart(4, "0"));
|
|
100
|
+
hashField(hash, payload);
|
|
101
|
+
}
|
|
102
|
+
/** Hash every directory, regular file, executable bit, and internal symlink in
|
|
103
|
+
* a dependency tree. Traversal uses raw UTF-8 byte ordering rather than the
|
|
104
|
+
* host locale, and rejects filesystem shapes that cannot be copied safely. */
|
|
105
|
+
export function dependencySnapshotTreeHash(nodeModules) {
|
|
106
|
+
const rootStat = lstatSync(nodeModules);
|
|
107
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
108
|
+
throw new Error("dependency snapshot node_modules root is not a real directory");
|
|
109
|
+
}
|
|
110
|
+
const hash = createHash("sha256");
|
|
111
|
+
hashField(hash, TREE_HASH_VERSION);
|
|
112
|
+
treeRecord(hash, "", "directory", rootStat.mode);
|
|
113
|
+
const walk = (dir, relative) => {
|
|
114
|
+
for (const name of treeEntries(dir)) {
|
|
115
|
+
const absolute = join(dir, name);
|
|
116
|
+
const next = relative ? `${relative}/${name}` : name;
|
|
117
|
+
const stat = lstatSync(absolute);
|
|
118
|
+
if (stat.isSymbolicLink()) {
|
|
119
|
+
const target = symlinkTarget(absolute);
|
|
120
|
+
safeInternalSymlink(nodeModules, absolute, target);
|
|
121
|
+
treeRecord(hash, next, "symlink", stat.mode, target);
|
|
122
|
+
}
|
|
123
|
+
else if (stat.isDirectory()) {
|
|
124
|
+
treeRecord(hash, next, "directory", stat.mode);
|
|
125
|
+
walk(absolute, next);
|
|
126
|
+
}
|
|
127
|
+
else if (stat.isFile()) {
|
|
128
|
+
treeRecord(hash, next, "file", stat.mode, fileSha256(absolute));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
throw new Error(`dependency snapshot contains special filesystem entry ${next}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
walk(nodeModules, "");
|
|
136
|
+
return `sha256:${hash.digest("hex")}`;
|
|
137
|
+
}
|
|
138
|
+
function copyDependencyTree(source, destination) {
|
|
139
|
+
const copyDirectory = (sourceDir, destinationDir) => {
|
|
140
|
+
const sourceStat = lstatSync(sourceDir);
|
|
141
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) {
|
|
142
|
+
throw new Error("dependency snapshot copy source is not a real directory");
|
|
143
|
+
}
|
|
144
|
+
mkdirSync(destinationDir, { mode: sourceStat.mode & 0o7777 });
|
|
145
|
+
for (const name of treeEntries(sourceDir)) {
|
|
146
|
+
const sourceEntry = join(sourceDir, name);
|
|
147
|
+
const destinationEntry = join(destinationDir, name);
|
|
148
|
+
const stat = lstatSync(sourceEntry);
|
|
149
|
+
if (stat.isSymbolicLink()) {
|
|
150
|
+
const target = symlinkTarget(sourceEntry);
|
|
151
|
+
const targetType = safeInternalSymlink(source, sourceEntry, target);
|
|
152
|
+
symlinkSync(target, destinationEntry, process.platform === "win32" ? targetType : undefined);
|
|
153
|
+
}
|
|
154
|
+
else if (stat.isDirectory()) {
|
|
155
|
+
copyDirectory(sourceEntry, destinationEntry);
|
|
156
|
+
}
|
|
157
|
+
else if (stat.isFile()) {
|
|
158
|
+
copyFileSync(sourceEntry, destinationEntry, fsConstants.COPYFILE_FICLONE);
|
|
159
|
+
chmodSync(destinationEntry, stat.mode & 0o7777);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
throw new Error("dependency snapshot contains a special filesystem entry");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
chmodSync(destinationDir, sourceStat.mode & 0o7777);
|
|
166
|
+
};
|
|
167
|
+
copyDirectory(source, destination);
|
|
168
|
+
}
|
|
169
|
+
/** Create one independently writable dependency tree. COPYFILE_FICLONE is a
|
|
170
|
+
* copy-on-write optimization where supported, never a shared hardlink; the
|
|
171
|
+
* byte-copy fallback has the same isolation. Pre/post hashes bind the copy. */
|
|
172
|
+
export function materializeDependencyTree(source, destination, opts = {}) {
|
|
173
|
+
if (existsSync(destination))
|
|
174
|
+
throw new Error("dependency snapshot destination already exists");
|
|
175
|
+
const sourceHash = dependencySnapshotTreeHash(source);
|
|
176
|
+
if (opts.expectedHash && sourceHash !== opts.expectedHash) {
|
|
177
|
+
throw new Error("dependency tree hash mismatch before materialization");
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
copyDependencyTree(source, destination);
|
|
181
|
+
if (dependencySnapshotTreeHash(destination) !== sourceHash) {
|
|
182
|
+
throw new Error("dependency tree hash mismatch after materialization");
|
|
183
|
+
}
|
|
184
|
+
return sourceHash;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
rmSync(destination, { recursive: true, force: true });
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Materialize a private writable dependency tree for one disposable run. */
|
|
192
|
+
export function materializeDependencySnapshot(dependency, destination) {
|
|
193
|
+
materializeDependencyTree(dependency.nodeModules, destination, {
|
|
194
|
+
expectedHash: dependency.snapshot.node_modules_hash,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function gitFile(root, commit, file, env) {
|
|
198
|
+
if (!FULL_SHA.test(commit) || replacementFreeExactCommit(root, commit) !== commit) {
|
|
199
|
+
throw new Error(`dependency snapshot commit ${commit} is not one exact commit`);
|
|
200
|
+
}
|
|
17
201
|
try {
|
|
18
202
|
return execFileSync("git", ["-C", root, "show", `${commit}:${file}`], {
|
|
19
203
|
encoding: "utf8",
|
|
204
|
+
env: replacementFreeGitEnvironment(env),
|
|
20
205
|
maxBuffer: 20 * 1024 * 1024,
|
|
21
206
|
stdio: ["ignore", "pipe", "ignore"],
|
|
22
207
|
});
|
|
@@ -62,7 +247,7 @@ function lockedPackageNames(lock) {
|
|
|
62
247
|
return names;
|
|
63
248
|
}
|
|
64
249
|
function normalizeAllowlist(values, lock) {
|
|
65
|
-
const result = [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
|
|
250
|
+
const result = [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort(compareCodeUnits);
|
|
66
251
|
const locked = lockedPackageNames(lock);
|
|
67
252
|
for (const name of result) {
|
|
68
253
|
if (!PACKAGE_NAME.test(name))
|
|
@@ -104,8 +289,8 @@ function runtimeIdentity(env) {
|
|
|
104
289
|
return { node: process.version, npm: result.stdout.trim(), platform: process.platform, arch: process.arch };
|
|
105
290
|
}
|
|
106
291
|
function snapshotInput(root, commit, allowInstallScripts, env) {
|
|
107
|
-
const packageJson = gitFile(root, commit, "package.json");
|
|
108
|
-
const packageLock = gitFile(root, commit, "package-lock.json");
|
|
292
|
+
const packageJson = gitFile(root, commit, "package.json", env);
|
|
293
|
+
const packageLock = gitFile(root, commit, "package-lock.json", env);
|
|
109
294
|
const pkg = parseObject(packageJson, "package.json");
|
|
110
295
|
const lock = parseObject(packageLock, "package-lock.json");
|
|
111
296
|
if (lock.lockfileVersion !== 2 && lock.lockfileVersion !== 3) {
|
|
@@ -139,7 +324,7 @@ function snapshotInput(root, commit, allowInstallScripts, env) {
|
|
|
139
324
|
function nativeInventory(nodeModules) {
|
|
140
325
|
const files = [];
|
|
141
326
|
const walk = (dir, relative) => {
|
|
142
|
-
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name
|
|
327
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => compareCodeUnits(a.name, b.name))) {
|
|
143
328
|
const absolute = join(dir, entry.name);
|
|
144
329
|
const next = relative ? `${relative}/${entry.name}` : entry.name;
|
|
145
330
|
if (entry.isSymbolicLink())
|
|
@@ -171,6 +356,8 @@ function readSnapshot(dir) {
|
|
|
171
356
|
const installedLock = join(nodeModules, ".package-lock.json");
|
|
172
357
|
if (!existsSync(nodeModules) || !lstatSync(nodeModules).isDirectory())
|
|
173
358
|
return null;
|
|
359
|
+
if (dependencySnapshotTreeHash(nodeModules) !== value.node_modules_hash)
|
|
360
|
+
return null;
|
|
174
361
|
const installedLockHash = existsSync(installedLock) ? sha256(readFileSync(installedLock)) : sha256("");
|
|
175
362
|
if (installedLockHash !== value.installed_lock_hash)
|
|
176
363
|
return null;
|
|
@@ -232,6 +419,7 @@ function buildSnapshot(base, input, env, timeoutMs) {
|
|
|
232
419
|
const nodeModules = join(work, "node_modules");
|
|
233
420
|
const installedLock = join(nodeModules, ".package-lock.json");
|
|
234
421
|
mkdirSync(nodeModules, { recursive: true });
|
|
422
|
+
const nodeModulesHash = dependencySnapshotTreeHash(nodeModules);
|
|
235
423
|
const body = {
|
|
236
424
|
input_hash: input.inputHash,
|
|
237
425
|
package_json_hash: input.packageJsonHash,
|
|
@@ -242,6 +430,7 @@ function buildSnapshot(base, input, env, timeoutMs) {
|
|
|
242
430
|
allow_install_scripts: input.allowInstallScripts,
|
|
243
431
|
installed_lock_hash: existsSync(installedLock) ? sha256(readFileSync(installedLock)) : sha256(""),
|
|
244
432
|
native_binaries: nativeInventory(nodeModules),
|
|
433
|
+
node_modules_hash: nodeModulesHash,
|
|
245
434
|
format_version: SNAPSHOT_VERSION,
|
|
246
435
|
data_class: "private",
|
|
247
436
|
authority: "none",
|
|
@@ -298,8 +487,8 @@ export function dependencySnapshotForCommit(root, commit, allowedIds) {
|
|
|
298
487
|
let packageJson;
|
|
299
488
|
let packageLock;
|
|
300
489
|
try {
|
|
301
|
-
packageJson = gitFile(root, commit, "package.json");
|
|
302
|
-
packageLock = gitFile(root, commit, "package-lock.json");
|
|
490
|
+
packageJson = gitFile(root, commit, "package.json", env);
|
|
491
|
+
packageLock = gitFile(root, commit, "package-lock.json", env);
|
|
303
492
|
}
|
|
304
493
|
catch {
|
|
305
494
|
return null;
|
|
@@ -343,7 +532,7 @@ export function provisionG2BehaviorDependencySnapshotsForCommits(root, commits,
|
|
|
343
532
|
});
|
|
344
533
|
return {
|
|
345
534
|
snapshots: [...new Map([...byInput.values()].map((snapshot) => [snapshot.id, snapshot])).values()]
|
|
346
|
-
.sort((left, right) => left.id
|
|
535
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
347
536
|
commits: mapped,
|
|
348
537
|
};
|
|
349
538
|
}
|
|
@@ -367,7 +556,7 @@ export function provisionG2BehaviorDependencySnapshots(root, report, candidate,
|
|
|
367
556
|
known_bad: { commit: candidate.proposed_corpus.known_bad.ref, dependency_snapshot_id: bad.dependency_snapshot_id },
|
|
368
557
|
known_good: { commit: candidate.proposed_corpus.known_good.ref, dependency_snapshot_id: good.dependency_snapshot_id },
|
|
369
558
|
},
|
|
370
|
-
allow_install_scripts: [...new Set(allowInstallScripts.map((value) => value.trim()).filter(Boolean))].sort(),
|
|
559
|
+
allow_install_scripts: [...new Set(allowInstallScripts.map((value) => value.trim()).filter(Boolean))].sort(compareCodeUnits),
|
|
371
560
|
data_class: "private",
|
|
372
561
|
authority: "none",
|
|
373
562
|
effects: "cache_only",
|
|
@@ -77,7 +77,7 @@ export function buildG2CandidateReview(store, root, opts = {}, resolutions = [])
|
|
|
77
77
|
const graphStore = new HunchStore(hunchPathsForDir(scratchRoot));
|
|
78
78
|
try {
|
|
79
79
|
graphStore.json.ensureDirs();
|
|
80
|
-
indexRepo(graphStore, root, { churn: false });
|
|
80
|
+
indexRepo(graphStore, root, { churn: false, requireComplete: true });
|
|
81
81
|
return buildFromIndexedGraph(store, graphStore, root, opts, resolutions);
|
|
82
82
|
}
|
|
83
83
|
finally {
|
|
@@ -24,11 +24,25 @@ export function blockingEvidenceError(proof, dispositions = []) {
|
|
|
24
24
|
return assessHistoryDispositions(proof, dispositions).blocking_error;
|
|
25
25
|
}
|
|
26
26
|
const proofRank = { P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5 };
|
|
27
|
+
/** Activation is a runtime property, not merely an approval-time check. The
|
|
28
|
+
* audit fallback keeps correction policies compiled before `origin` was added
|
|
29
|
+
* fail-closed after they are loaded by the current schema. */
|
|
30
|
+
export function activationGateError(policy) {
|
|
31
|
+
const isMd1Correction = policy.origin === "correction_md1a"
|
|
32
|
+
|| policy.audit.some((event) => event.action === "compiled" && event.actor === "hunch:correction-policy-materializer");
|
|
33
|
+
if (isMd1Correction && policy.activation_gate?.status !== "blocked") {
|
|
34
|
+
return "MD-1a correction policy is missing its required source-currentness activation gate";
|
|
35
|
+
}
|
|
36
|
+
return policy.activation_gate?.status === "blocked" ? policy.activation_gate.reason : null;
|
|
37
|
+
}
|
|
27
38
|
/** Rechecked on every blocking evaluation; a hand-edited lifecycle flag without
|
|
28
39
|
* a current P3 proof is a configuration error, never authority. */
|
|
29
40
|
export function blockingProofError(policy, proof, dispositions = [], composition = [], currentBehaviorAttestations = []) {
|
|
30
41
|
if (policy.state !== "active_blocking")
|
|
31
42
|
return null;
|
|
43
|
+
const activationError = activationGateError(policy);
|
|
44
|
+
if (activationError)
|
|
45
|
+
return activationError;
|
|
32
46
|
if (policy.authority?.kind !== "human")
|
|
33
47
|
return "active blocking policy has no human authority event";
|
|
34
48
|
const behaviorAttestationError = executableBehaviorAttestationError(policy, currentBehaviorAttestations);
|
|
@@ -95,6 +109,9 @@ export function proposeProvedPolicy(policy, proof, at, composition = [], current
|
|
|
95
109
|
};
|
|
96
110
|
}
|
|
97
111
|
export function approvePolicy(policy, proof, mode, actor, at, dispositions = [], composition = [], currentBehaviorAttestations = []) {
|
|
112
|
+
const activationError = activationGateError(policy);
|
|
113
|
+
if (activationError)
|
|
114
|
+
throw new Error(`policy ${policy.id} cannot activate: ${activationError}`);
|
|
98
115
|
requireHuman(actor);
|
|
99
116
|
const behaviorAttestationError = executableBehaviorAttestationError(policy, currentBehaviorAttestations);
|
|
100
117
|
if (behaviorAttestationError)
|