@davesheffer/hunch 1.7.0 → 1.7.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 +214 -0
- package/bench/constitution-exp03-v1.json +70 -0
- package/dist/cli/index.js +1203 -24
- package/dist/constitution/adapters.js +487 -0
- package/dist/constitution/behaviorAttestationBinding.js +17 -0
- package/dist/constitution/behaviorEvaluator.js +220 -0
- package/dist/constitution/behaviorProof.js +205 -0
- package/dist/constitution/behaviorWorkspace.js +124 -0
- package/dist/constitution/bootstrap.js +133 -0
- package/dist/constitution/canonical.js +51 -0
- package/dist/constitution/card.js +133 -0
- package/dist/constitution/compiler.js +176 -0
- package/dist/constitution/composition.js +101 -0
- package/dist/constitution/corpus.js +58 -0
- package/dist/constitution/delta.js +154 -0
- package/dist/constitution/disposition.js +141 -0
- package/dist/constitution/evaluator.js +435 -0
- package/dist/constitution/experiment.js +948 -0
- package/dist/constitution/experimentRunner.js +344 -0
- package/dist/constitution/g2.js +291 -0
- package/dist/constitution/g2BehaviorAttestation.js +209 -0
- package/dist/constitution/g2BehaviorCandidates.js +703 -0
- package/dist/constitution/g2BehaviorDependencies.js +379 -0
- package/dist/constitution/g2BehaviorMaterialization.js +171 -0
- package/dist/constitution/g2BehaviorPolicyMaterializer.js +241 -0
- package/dist/constitution/g2CandidateAttestation.js +179 -0
- package/dist/constitution/g2Candidates.js +195 -0
- package/dist/constitution/g2Drills.js +122 -0
- package/dist/constitution/g3.js +511 -0
- package/dist/constitution/g3Conformance.js +115 -0
- package/dist/constitution/lifecycle.js +189 -0
- package/dist/constitution/mutation.js +262 -0
- package/dist/constitution/nodeTestEvidence.js +47 -0
- package/dist/constitution/plan.js +172 -0
- package/dist/constitution/policyRuntime.js +8 -0
- package/dist/constitution/proof.js +166 -0
- package/dist/constitution/replay.js +361 -0
- package/dist/constitution/replayCache.js +89 -0
- package/dist/constitution/replayWorker.js +34 -0
- package/dist/constitution/repository.js +533 -0
- package/dist/constitution/schema.js +545 -0
- package/dist/constitution/scorecard.js +106 -0
- package/dist/constitution/service.js +1149 -0
- package/dist/constitution/shadow.js +235 -0
- package/dist/constitution/sourceMutation.js +316 -0
- package/dist/constitution/structural.js +601 -0
- package/dist/core/autoreview.js +27 -3
- package/dist/core/dupdetect.js +10 -3
- package/dist/core/events.js +61 -0
- package/dist/core/externalImports.js +24 -0
- package/dist/core/hookpolicy.js +3 -0
- package/dist/core/relativeImports.js +33 -0
- package/dist/core/stats.js +115 -0
- package/dist/extractors/git.js +81 -0
- package/dist/extractors/indexer.js +39 -38
- package/dist/extractors/nativeTreeSitter.js +108 -0
- package/dist/extractors/parse.js +5 -15
- package/dist/integrations/claudemd.js +8 -1
- package/dist/integrations/gitignore.js +8 -0
- package/dist/integrations/providers.js +32 -10
- package/dist/integrations/sync.js +16 -1
- package/dist/mcp/server.js +284 -0
- package/package.json +5 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { headSha, revExists } from "../extractors/git.js";
|
|
7
|
+
import { canonicalHash } from "./canonical.js";
|
|
8
|
+
import { nodeTestInfrastructureError } from "./g2BehaviorCandidates.js";
|
|
9
|
+
import { dependencySnapshotForCommit } from "./g2BehaviorDependencies.js";
|
|
10
|
+
import { cleanupReplayWorktree, hasUnsafeReplayFilter, replayGitArgs } from "./replay.js";
|
|
11
|
+
import { BEHAVIOR_POLICY_EVALUATOR, PolicyEvaluationSchema, } from "./schema.js";
|
|
12
|
+
import { NODE_TEST_REPORTER_SOURCE, exactNodeTestPattern, nodeTestIsolationFlag, nodeTestReporterEvents, } from "./nodeTestEvidence.js";
|
|
13
|
+
import { applyBehaviorWorkspace, BehaviorWorkspaceError, captureBehaviorWorkspace, } from "./behaviorWorkspace.js";
|
|
14
|
+
function behaviorEnvironment(home, gitConfig) {
|
|
15
|
+
const env = {};
|
|
16
|
+
for (const key of ["PATH", "SystemRoot", "WINDIR", "TMPDIR", "TMP", "TEMP"]) {
|
|
17
|
+
if (process.env[key])
|
|
18
|
+
env[key] = process.env[key];
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
...env,
|
|
22
|
+
HOME: home,
|
|
23
|
+
GIT_CONFIG_GLOBAL: gitConfig,
|
|
24
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
25
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
26
|
+
GIT_LFS_SKIP_SMUDGE: "1",
|
|
27
|
+
HUNCH_PRIVATE_DIR: "",
|
|
28
|
+
HUNCH_SYNTH_PROVIDER: "deterministic",
|
|
29
|
+
CI: "1",
|
|
30
|
+
NO_COLOR: "1",
|
|
31
|
+
FORCE_COLOR: "0",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function sourceAt(root, commit, file) {
|
|
35
|
+
try {
|
|
36
|
+
return execFileSync("git", ["-C", root, "show", `${commit}:${file}`], {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
39
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function evaluation(policy, commit, execution, result, explanation) {
|
|
47
|
+
const executionHash = canonicalHash(execution);
|
|
48
|
+
const behavior = { ...execution, execution_hash: executionHash };
|
|
49
|
+
const repository = behavior.workspace
|
|
50
|
+
? { base: commit, head: `${behavior.workspace.kind}:${behavior.workspace.snapshot_hash}`, graph_hash: executionHash }
|
|
51
|
+
: { head: commit, graph_hash: executionHash };
|
|
52
|
+
const body = {
|
|
53
|
+
policy_id: policy.id,
|
|
54
|
+
policy_revision: policy.revision,
|
|
55
|
+
result,
|
|
56
|
+
evaluator: { ...BEHAVIOR_POLICY_EVALUATOR },
|
|
57
|
+
repository,
|
|
58
|
+
matches: [{ file: behavior.test.file, symbol: behavior.test.name }],
|
|
59
|
+
explanation,
|
|
60
|
+
evidence_refs: [...policy.evidence],
|
|
61
|
+
behavior,
|
|
62
|
+
};
|
|
63
|
+
return PolicyEvaluationSchema.parse({ ...body, deterministic_hash: canonicalHash(body) });
|
|
64
|
+
}
|
|
65
|
+
export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
|
|
66
|
+
if (policy.assertion.kind !== "executable-behavior") {
|
|
67
|
+
throw new Error(`policy ${policy.id} is not an executable-behavior policy`);
|
|
68
|
+
}
|
|
69
|
+
const assertion = policy.assertion;
|
|
70
|
+
if (opts.commit && opts.workspace)
|
|
71
|
+
throw new Error("executable behavior evaluation accepts either a commit or a workspace snapshot, not both");
|
|
72
|
+
const commit = opts.commit ?? headSha(root);
|
|
73
|
+
const selectedName = opts.selectedName ?? assertion.test.name;
|
|
74
|
+
let workspace;
|
|
75
|
+
if (opts.workspace && /^[a-f0-9]{40}$/.test(commit)) {
|
|
76
|
+
const probe = mkdtempSync(join(tmpdir(), "hunch-behavior-workspace-probe-"));
|
|
77
|
+
const env = behaviorEnvironment(probe, join(probe, "global.gitconfig"));
|
|
78
|
+
try {
|
|
79
|
+
writeFileSync(join(probe, "global.gitconfig"), "");
|
|
80
|
+
workspace = captureBehaviorWorkspace(root, commit, opts.workspace, env);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const code = error instanceof BehaviorWorkspaceError ? error.code : "workspace-snapshot-failed";
|
|
84
|
+
const failed = {
|
|
85
|
+
commit,
|
|
86
|
+
test: { file: assertion.test.file, name: selectedName, source_hash: assertion.test.source_hash },
|
|
87
|
+
runner: assertion.runner,
|
|
88
|
+
exit_code: null,
|
|
89
|
+
selected_event: null,
|
|
90
|
+
error_code: code,
|
|
91
|
+
};
|
|
92
|
+
return evaluation(policy, commit, failed, "error", `executable behavior ${opts.workspace} workspace snapshot failed (${code})`);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
rmSync(probe, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const baseExecution = {
|
|
99
|
+
commit: /^[a-f0-9]{40}$/.test(commit) ? commit : "0".repeat(40),
|
|
100
|
+
...(workspace ? { workspace: workspace.metadata } : {}),
|
|
101
|
+
test: { file: assertion.test.file, name: selectedName, source_hash: assertion.test.source_hash },
|
|
102
|
+
runner: assertion.runner,
|
|
103
|
+
exit_code: null,
|
|
104
|
+
selected_event: null,
|
|
105
|
+
};
|
|
106
|
+
if (!/^[a-f0-9]{40}$/.test(commit) || !revExists(commit, root)) {
|
|
107
|
+
return evaluation(policy, baseExecution.commit, { ...baseExecution, error_code: "commit-ref-unresolved" }, "error", "executable behavior commit does not resolve");
|
|
108
|
+
}
|
|
109
|
+
const source = sourceAt(root, assertion.test.source_commit, assertion.test.file);
|
|
110
|
+
if (source == null || canonicalHash(source) !== assertion.test.source_hash) {
|
|
111
|
+
return evaluation(policy, commit, { ...baseExecution, commit, error_code: "test-source-hash-mismatch" }, "error", "pinned behavior test source does not match its exact commit/hash binding");
|
|
112
|
+
}
|
|
113
|
+
if (workspace?.metadata.files.some((file) => file === "package.json" || file === "package-lock.json")) {
|
|
114
|
+
return evaluation(policy, commit, { ...baseExecution, commit, error_code: "workspace-dependency-input-changed" }, "error", "workspace dependency inputs changed; provision an exact dependency snapshot after commit");
|
|
115
|
+
}
|
|
116
|
+
const dependency = dependencySnapshotForCommit(root, commit, assertion.dependency_snapshot_ids);
|
|
117
|
+
if (!dependency) {
|
|
118
|
+
return evaluation(policy, commit, { ...baseExecution, commit, error_code: "dependency-snapshot-unavailable" }, "error", "no unique exact dependency snapshot is available for executable behavior evaluation");
|
|
119
|
+
}
|
|
120
|
+
const session = mkdtempSync(join(tmpdir(), "hunch-behavior-policy-"));
|
|
121
|
+
const hooks = join(session, "hooks-disabled");
|
|
122
|
+
const gitConfig = join(session, "global.gitconfig");
|
|
123
|
+
const checkout = join(session, "checkout");
|
|
124
|
+
const reporter = join(session, "reporter.mjs");
|
|
125
|
+
mkdirSync(hooks, { recursive: true });
|
|
126
|
+
writeFileSync(gitConfig, "");
|
|
127
|
+
writeFileSync(reporter, NODE_TEST_REPORTER_SOURCE);
|
|
128
|
+
const env = behaviorEnvironment(session, gitConfig);
|
|
129
|
+
let added = false;
|
|
130
|
+
let result;
|
|
131
|
+
try {
|
|
132
|
+
if (hasUnsafeReplayFilter(root, env)) {
|
|
133
|
+
result = evaluation(policy, commit, { ...baseExecution, commit, dependency_snapshot_id: dependency.snapshot.id, error_code: "unsafe-local-filter-config" }, "error", "repository has an unsafe local Git content filter");
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
execFileSync("git", replayGitArgs(root, hooks, ["worktree", "add", "--detach", "--force", checkout, commit]), {
|
|
137
|
+
env,
|
|
138
|
+
timeout: assertion.timeout_ms,
|
|
139
|
+
stdio: "ignore",
|
|
140
|
+
});
|
|
141
|
+
added = true;
|
|
142
|
+
if (workspace)
|
|
143
|
+
applyBehaviorWorkspace(root, checkout, workspace, env);
|
|
144
|
+
symlinkSync(dependency.nodeModules, join(checkout, "node_modules"), process.platform === "win32" ? "junction" : "dir");
|
|
145
|
+
const testFile = join(checkout, assertion.test.file);
|
|
146
|
+
mkdirSync(dirname(testFile), { recursive: true });
|
|
147
|
+
writeFileSync(testFile, source);
|
|
148
|
+
const common = [
|
|
149
|
+
"--test",
|
|
150
|
+
nodeTestIsolationFlag(),
|
|
151
|
+
`--test-name-pattern=${exactNodeTestPattern(selectedName)}`,
|
|
152
|
+
`--test-reporter=${pathToFileURL(reporter).href}`,
|
|
153
|
+
"--test-reporter-destination=stdout",
|
|
154
|
+
assertion.test.file,
|
|
155
|
+
];
|
|
156
|
+
const args = assertion.runner === "node-test"
|
|
157
|
+
? common
|
|
158
|
+
: [join(dependency.nodeModules, "tsx", "dist", "cli.mjs"), ...common];
|
|
159
|
+
const run = spawnSync(process.execPath, args, {
|
|
160
|
+
cwd: checkout,
|
|
161
|
+
env,
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
timeout: assertion.timeout_ms,
|
|
164
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
165
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
166
|
+
shell: false,
|
|
167
|
+
});
|
|
168
|
+
const output = `${run.stdout ?? ""}\n${run.stderr ?? ""}`;
|
|
169
|
+
const matches = nodeTestReporterEvents(run.stdout ?? "")
|
|
170
|
+
.filter((event) => event.name === selectedName && !event.skip && !event.todo);
|
|
171
|
+
const exitCode = run.status ?? null;
|
|
172
|
+
const execution = {
|
|
173
|
+
...baseExecution,
|
|
174
|
+
commit,
|
|
175
|
+
dependency_snapshot_id: dependency.snapshot.id,
|
|
176
|
+
exit_code: exitCode,
|
|
177
|
+
selected_event: matches.length === 1 ? (matches[0].type === "test:pass" ? "passed" : "failed") : null,
|
|
178
|
+
};
|
|
179
|
+
if (run.error) {
|
|
180
|
+
const code = run.error.code === "ETIMEDOUT" ? "timeout" : "runner-failed";
|
|
181
|
+
result = evaluation(policy, commit, { ...execution, error_code: code }, "error", `executable behavior runner ${code}`);
|
|
182
|
+
}
|
|
183
|
+
else if (run.signal) {
|
|
184
|
+
result = evaluation(policy, commit, { ...execution, error_code: "runner-signaled" }, "error", `executable behavior runner exited by ${run.signal}`);
|
|
185
|
+
}
|
|
186
|
+
else if (matches.length === 0) {
|
|
187
|
+
const infrastructure = nodeTestInfrastructureError(output) ?? "selected-test-not-executed";
|
|
188
|
+
result = evaluation(policy, commit, { ...execution, error_code: infrastructure }, "error", `exact selected test produced no executed pass/fail event (${infrastructure})`);
|
|
189
|
+
}
|
|
190
|
+
else if (matches.length > 1) {
|
|
191
|
+
result = evaluation(policy, commit, { ...execution, error_code: "selected-test-ambiguous" }, "error", "exact selected test name produced multiple executed events");
|
|
192
|
+
}
|
|
193
|
+
else if (matches[0].type === "test:pass" && exitCode === 0) {
|
|
194
|
+
result = evaluation(policy, commit, execution, "satisfied", `exact selected executable behavior test passed${opts.workspace ? ` in ${opts.workspace} workspace snapshot` : ""}`);
|
|
195
|
+
}
|
|
196
|
+
else if (matches[0].type === "test:fail" && exitCode !== 0) {
|
|
197
|
+
result = evaluation(policy, commit, execution, "violated", `exact selected executable behavior test failed${opts.workspace ? ` in ${opts.workspace} workspace snapshot` : ""}`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
result = evaluation(policy, commit, { ...execution, error_code: "runner-outcome-inconsistent" }, "error", "selected test event and runner exit code disagree");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const code = error instanceof BehaviorWorkspaceError
|
|
206
|
+
? error.code
|
|
207
|
+
: error.code === "ETIMEDOUT" ? "timeout" : added ? "runner-setup-failed" : "worktree-create-failed";
|
|
208
|
+
result = evaluation(policy, commit, { ...baseExecution, commit, dependency_snapshot_id: dependency.snapshot.id, error_code: code }, "error", `executable behavior evaluation failed during isolated setup (${code})`);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
if (added && cleanupReplayWorktree(root, hooks, env, session, checkout)) {
|
|
212
|
+
result = evaluation(policy, commit, { ...baseExecution, commit, dependency_snapshot_id: dependency.snapshot.id, error_code: "worktree-cleanup-failed" }, "error", "executable behavior worktree cleanup failed");
|
|
213
|
+
}
|
|
214
|
+
else if (!added) {
|
|
215
|
+
rmSync(session, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=behaviorEvaluator.js.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { shortHash } from "../core/ids.js";
|
|
3
|
+
import { headSha } from "../extractors/git.js";
|
|
4
|
+
import { evaluateExecutableBehaviorPolicy } from "./behaviorEvaluator.js";
|
|
5
|
+
import { canonicalHash, policySemanticHash, proofEvaluationHash, proofId, proofPlanContentHash } from "./canonical.js";
|
|
6
|
+
import { BEHAVIOR_MUTATION_ENGINE, BEHAVIOR_POLICY_EVALUATOR, MutationReceiptSchema, PolicyProofSchema, ProofPlanSchema, ReplayReceiptSchema, } from "./schema.js";
|
|
7
|
+
function summary(evaluations) {
|
|
8
|
+
const count = (result) => evaluations.filter((evaluation) => evaluation.result === result).length;
|
|
9
|
+
return {
|
|
10
|
+
total: evaluations.length,
|
|
11
|
+
satisfied: count("satisfied"),
|
|
12
|
+
violated: count("violated"),
|
|
13
|
+
not_applicable: count("not_applicable"),
|
|
14
|
+
unknown: count("unknown"),
|
|
15
|
+
error: count("error"),
|
|
16
|
+
receipt_hashes: evaluations.map(proofEvaluationHash),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function replayReceipt(leg, policyHash, expected, evaluation) {
|
|
20
|
+
const body = {
|
|
21
|
+
leg,
|
|
22
|
+
commit: evaluation.behavior.commit,
|
|
23
|
+
expected,
|
|
24
|
+
policy_hash: policyHash,
|
|
25
|
+
evaluator: { ...BEHAVIOR_POLICY_EVALUATOR },
|
|
26
|
+
result: evaluation.result,
|
|
27
|
+
graph_hash: evaluation.behavior.execution_hash,
|
|
28
|
+
evaluation_hash: proofEvaluationHash(evaluation),
|
|
29
|
+
...(evaluation.behavior.error_code ? { error_code: evaluation.behavior.error_code } : {}),
|
|
30
|
+
behavior: evaluation.behavior,
|
|
31
|
+
};
|
|
32
|
+
return ReplayReceiptSchema.parse({ ...body, deterministic_hash: canonicalHash(body) });
|
|
33
|
+
}
|
|
34
|
+
function mutationReceipt(policyHash, planned, kind, evaluation, passed) {
|
|
35
|
+
const seed = {
|
|
36
|
+
engine: BEHAVIOR_MUTATION_ENGINE,
|
|
37
|
+
policy_hash: policyHash,
|
|
38
|
+
base_commit: evaluation.behavior.commit,
|
|
39
|
+
base_graph_hash: evaluation.behavior.execution_hash,
|
|
40
|
+
kind,
|
|
41
|
+
operator: planned.operator,
|
|
42
|
+
};
|
|
43
|
+
const body = {
|
|
44
|
+
id: `mut_${shortHash(canonicalHash(seed))}`,
|
|
45
|
+
kind,
|
|
46
|
+
operator: planned.operator,
|
|
47
|
+
required: planned.required,
|
|
48
|
+
engine: { ...BEHAVIOR_MUTATION_ENGINE },
|
|
49
|
+
policy_hash: policyHash,
|
|
50
|
+
base_commit: evaluation.behavior.commit,
|
|
51
|
+
base_graph_hash: evaluation.behavior.execution_hash,
|
|
52
|
+
mutated_graph_hash: evaluation.behavior.execution_hash,
|
|
53
|
+
expected: planned.expected,
|
|
54
|
+
result: evaluation.result,
|
|
55
|
+
passed,
|
|
56
|
+
parseability: "not_applicable",
|
|
57
|
+
graph_diff: { added_symbols: [], removed_symbols: [], added_edges: [], removed_edges: [] },
|
|
58
|
+
evaluation_hash: proofEvaluationHash(evaluation),
|
|
59
|
+
...(evaluation.behavior.error_code ? { error_code: evaluation.behavior.error_code } : {}),
|
|
60
|
+
behavior: evaluation.behavior,
|
|
61
|
+
};
|
|
62
|
+
return MutationReceiptSchema.parse({ ...body, deterministic_hash: canonicalHash(body) });
|
|
63
|
+
}
|
|
64
|
+
export function createExecutableBehaviorProofPlan(root, repository, policy, opts = {}) {
|
|
65
|
+
if (policy.assertion.kind !== "executable-behavior")
|
|
66
|
+
throw new Error(`policy ${policy.id} is not executable behavior`);
|
|
67
|
+
if (policy.data_class === "public")
|
|
68
|
+
throw new Error("executable-behavior policy materialization requires private storage");
|
|
69
|
+
const head = headSha(root);
|
|
70
|
+
if (!/^[a-f0-9]{40}$/.test(head))
|
|
71
|
+
throw new Error("executable behavior proof planning requires a current full-SHA HEAD");
|
|
72
|
+
const corpus = repository.getCorpus(policy.id, { privateOnly: opts.privateOnly ?? true });
|
|
73
|
+
if (!corpus || corpus.known_bad.length !== 1 || corpus.known_good.length !== 1) {
|
|
74
|
+
throw new Error("executable behavior proof requires exactly one imported known-bad and one known-good commit");
|
|
75
|
+
}
|
|
76
|
+
const policyHash = policySemanticHash(policy);
|
|
77
|
+
if (corpus.policy_hash !== policyHash)
|
|
78
|
+
throw new Error(`behavior corpus ${corpus.id} is stale for policy ${policy.id}`);
|
|
79
|
+
const bad = corpus.known_bad[0];
|
|
80
|
+
const good = corpus.known_good[0];
|
|
81
|
+
const body = {
|
|
82
|
+
policy_id: policy.id,
|
|
83
|
+
policy_candidate_hash: policyHash,
|
|
84
|
+
repository: basename(root),
|
|
85
|
+
data_class: policy.data_class,
|
|
86
|
+
source_commit: policy.assertion.test.source_commit,
|
|
87
|
+
valid_from_commit: policy.assertion.test.source_commit,
|
|
88
|
+
evaluator: { ...BEHAVIOR_POLICY_EVALUATOR },
|
|
89
|
+
mutation_engine: { ...BEHAVIOR_MUTATION_ENGINE },
|
|
90
|
+
corpus_manifest: { id: corpus.id, content_hash: corpus.content_hash },
|
|
91
|
+
corpus: {
|
|
92
|
+
current_baseline: { kind: "commit", ref: head, label: "current committed executable behavior baseline", expected: "satisfied" },
|
|
93
|
+
accepted_history: { from: head, to: head, first_parent: true, max_commits: 0, exclude: [bad.ref, good.ref].sort() },
|
|
94
|
+
known_bad: [bad],
|
|
95
|
+
known_good: [good],
|
|
96
|
+
},
|
|
97
|
+
mutations: [
|
|
98
|
+
{ operator: "known-bad-regression", base: bad.ref, expected: "violated", required: true },
|
|
99
|
+
{ operator: "selected-test-execution-control", base: good.ref, expected: "error", required: true },
|
|
100
|
+
],
|
|
101
|
+
budgets: { max_commits: 0, max_mutations: 2, max_minutes: 5 },
|
|
102
|
+
expected: [
|
|
103
|
+
{ leg: "current_baseline", result: "satisfied", classification_required: false },
|
|
104
|
+
{ leg: "known_bad", result: "violated", classification_required: false },
|
|
105
|
+
{ leg: "known_good", result: "satisfied", classification_required: false },
|
|
106
|
+
{ leg: "mutations", result: "violated", classification_required: false },
|
|
107
|
+
],
|
|
108
|
+
evidence_refs: [...policy.evidence].sort(),
|
|
109
|
+
limitations: [
|
|
110
|
+
"Executable behavior proof runs only a hash-pinned node:test case through an allowlisted runner in disposable committed worktrees.",
|
|
111
|
+
"The proof baseline is committed HEAD; advisory delivery may separately evaluate a content-addressed staged or working snapshot.",
|
|
112
|
+
"Known-bad replay is the primary historical regression mutation; a missing-name control proves that zero executed tests cannot satisfy the evaluator.",
|
|
113
|
+
"Repository test code executes as evidence but grants no authority; policy activation remains a separate explicit human action.",
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
const contentHash = canonicalHash(body);
|
|
117
|
+
return ProofPlanSchema.parse({
|
|
118
|
+
id: `plan_${shortHash(contentHash)}`,
|
|
119
|
+
content_hash: contentHash,
|
|
120
|
+
...body,
|
|
121
|
+
created_at: opts.now ?? new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export function proveExecutableBehaviorPolicy(root, policy, plan, opts = {}) {
|
|
125
|
+
if (policy.assertion.kind !== "executable-behavior")
|
|
126
|
+
throw new Error(`policy ${policy.id} is not executable behavior`);
|
|
127
|
+
if (plan.content_hash !== proofPlanContentHash(plan))
|
|
128
|
+
throw new Error(`proof plan ${plan.id} content hash mismatch`);
|
|
129
|
+
const policyHash = policySemanticHash(policy);
|
|
130
|
+
if (plan.policy_id !== policy.id || plan.policy_candidate_hash !== policyHash)
|
|
131
|
+
throw new Error(`proof plan ${plan.id} does not match behavior policy semantics`);
|
|
132
|
+
if (plan.evaluator.name !== BEHAVIOR_POLICY_EVALUATOR.name || plan.evaluator.version !== BEHAVIOR_POLICY_EVALUATOR.version)
|
|
133
|
+
throw new Error(`proof plan ${plan.id} behavior evaluator is stale`);
|
|
134
|
+
if (plan.mutation_engine?.name !== BEHAVIOR_MUTATION_ENGINE.name || plan.mutation_engine.version !== BEHAVIOR_MUTATION_ENGINE.version)
|
|
135
|
+
throw new Error(`proof plan ${plan.id} behavior mutation engine is stale`);
|
|
136
|
+
const primaryPlan = plan.mutations.find((mutation) => mutation.operator === "known-bad-regression");
|
|
137
|
+
const controlPlan = plan.mutations.find((mutation) => mutation.operator === "selected-test-execution-control");
|
|
138
|
+
if (!primaryPlan || !controlPlan)
|
|
139
|
+
throw new Error("executable behavior proof plan lacks required mutation/control operators");
|
|
140
|
+
const current = evaluateExecutableBehaviorPolicy(root, policy, { commit: plan.corpus.current_baseline.ref });
|
|
141
|
+
const knownBad = plan.corpus.known_bad.map((fixture) => evaluateExecutableBehaviorPolicy(root, policy, { commit: fixture.ref }));
|
|
142
|
+
const knownGood = plan.corpus.known_good.map((fixture) => evaluateExecutableBehaviorPolicy(root, policy, { commit: fixture.ref }));
|
|
143
|
+
const controlName = `__hunch_missing_${shortHash(policyHash)}`;
|
|
144
|
+
const control = evaluateExecutableBehaviorPolicy(root, policy, { commit: controlPlan.base, selectedName: controlName });
|
|
145
|
+
const replayReceipts = [
|
|
146
|
+
replayReceipt("current_baseline", policyHash, "satisfied", current),
|
|
147
|
+
...knownBad.map((evaluation) => replayReceipt("known_bad", policyHash, "violated", evaluation)),
|
|
148
|
+
...knownGood.map((evaluation) => replayReceipt("known_good", policyHash, "satisfied", evaluation)),
|
|
149
|
+
];
|
|
150
|
+
const primaryEvaluation = knownBad.find((evaluation) => evaluation.behavior?.commit === primaryPlan.base) ?? knownBad[0];
|
|
151
|
+
const primaryReceipt = mutationReceipt(policyHash, primaryPlan, "primary", primaryEvaluation, primaryEvaluation.result === primaryPlan.expected);
|
|
152
|
+
const controlPassed = control.result === "error" && control.behavior?.error_code === "selected-test-not-executed";
|
|
153
|
+
const controlReceipt = mutationReceipt(policyHash, controlPlan, "control", control, controlPassed);
|
|
154
|
+
const mutationReceipts = [primaryReceipt, controlReceipt];
|
|
155
|
+
const currentSummary = summary([current]);
|
|
156
|
+
const badSummary = summary(knownBad);
|
|
157
|
+
const goodSummary = summary(knownGood);
|
|
158
|
+
const baselineSatisfied = currentSummary.satisfied === 1 && currentSummary.error === 0 && currentSummary.unknown === 0;
|
|
159
|
+
const exactBad = badSummary.total > 0 && badSummary.violated === badSummary.total;
|
|
160
|
+
const exactGood = goodSummary.total > 0 && goodSummary.satisfied === goodSummary.total;
|
|
161
|
+
const controlsPassed = mutationReceipts.every((receipt) => receipt.passed);
|
|
162
|
+
const proofClass = baselineSatisfied && exactBad && exactGood && controlsPassed ? "P3" : baselineSatisfied ? "P1" : "P0";
|
|
163
|
+
const generatedAt = opts.now ?? new Date().toISOString();
|
|
164
|
+
const id = proofId({ policy_hash: policyHash, plan_hash: plan.content_hash, evaluator: BEHAVIOR_POLICY_EVALUATOR });
|
|
165
|
+
return PolicyProofSchema.parse({
|
|
166
|
+
id,
|
|
167
|
+
plan_hash: plan.content_hash,
|
|
168
|
+
policy_hash: policyHash,
|
|
169
|
+
evaluator: { ...BEHAVIOR_POLICY_EVALUATOR },
|
|
170
|
+
mutation_engine: { ...BEHAVIOR_MUTATION_ENGINE },
|
|
171
|
+
generated_at: generatedAt,
|
|
172
|
+
current: currentSummary,
|
|
173
|
+
known_bad: badSummary,
|
|
174
|
+
known_good: goodSummary,
|
|
175
|
+
accepted_history: { ...summary([]), classified_hits: [] },
|
|
176
|
+
mutations: { ...summary([primaryEvaluation]), operator_coverage: { [primaryReceipt.operator]: primaryReceipt.passed ? 1 : 0 } },
|
|
177
|
+
replay_receipts: replayReceipts,
|
|
178
|
+
mutation_receipts: mutationReceipts,
|
|
179
|
+
mutation_controls: {
|
|
180
|
+
total: 1,
|
|
181
|
+
passed: controlReceipt.passed ? 1 : 0,
|
|
182
|
+
failed: controlReceipt.passed ? 0 : 1,
|
|
183
|
+
receipt_hashes: [controlReceipt.deterministic_hash],
|
|
184
|
+
},
|
|
185
|
+
project_checks: {
|
|
186
|
+
build: "not_run",
|
|
187
|
+
test: baselineSatisfied && exactBad && exactGood ? "passed" : "failed",
|
|
188
|
+
required_for_evaluator_sensitivity: true,
|
|
189
|
+
},
|
|
190
|
+
limitations: [...plan.limitations],
|
|
191
|
+
proof_class: proofClass,
|
|
192
|
+
artifact_hashes: {
|
|
193
|
+
policy: policyHash,
|
|
194
|
+
plan: plan.content_hash,
|
|
195
|
+
attestation: policy.assertion.attestation.content_hash,
|
|
196
|
+
replay: policy.assertion.attestation.replay_hash,
|
|
197
|
+
test_source: policy.assertion.test.source_hash,
|
|
198
|
+
replay_manifest: canonicalHash(replayReceipts),
|
|
199
|
+
mutation_manifest: canonicalHash(mutationReceipts),
|
|
200
|
+
current_receipt: replayReceipts[0].deterministic_hash,
|
|
201
|
+
},
|
|
202
|
+
data_class: policy.data_class,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=behaviorProof.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { canonicalHash } from "./canonical.js";
|
|
6
|
+
export class BehaviorWorkspaceError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const MAX_PATCH_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const MAX_UNTRACKED_FILES = 1_000;
|
|
15
|
+
const MAX_UNTRACKED_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
const WORKSPACE_PATHS = [".", ":(exclude).hunch/**", ":(exclude).hunch-cache/**"];
|
|
17
|
+
function bufferSha1(value) {
|
|
18
|
+
return `sha1:${createHash("sha1").update(value).digest("hex")}`;
|
|
19
|
+
}
|
|
20
|
+
function git(root, args, env, maxBuffer = MAX_PATCH_BYTES) {
|
|
21
|
+
const run = spawnSync("git", ["-C", root, ...args], {
|
|
22
|
+
env,
|
|
23
|
+
encoding: "buffer",
|
|
24
|
+
maxBuffer,
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
shell: false,
|
|
27
|
+
});
|
|
28
|
+
if (run.error) {
|
|
29
|
+
const code = run.error.code === "ENOBUFS" ? "workspace-snapshot-too-large" : "workspace-git-failed";
|
|
30
|
+
throw new BehaviorWorkspaceError(code, `workspace Git command failed: ${args[0]}`);
|
|
31
|
+
}
|
|
32
|
+
if (run.status !== 0)
|
|
33
|
+
throw new BehaviorWorkspaceError("workspace-git-failed", `workspace Git command exited ${run.status}: ${args[0]}`);
|
|
34
|
+
return run.stdout ?? Buffer.alloc(0);
|
|
35
|
+
}
|
|
36
|
+
function safeRelative(root, file) {
|
|
37
|
+
if (!file || file.includes("\0") || file.includes("\\") || isAbsolute(file)) {
|
|
38
|
+
throw new BehaviorWorkspaceError("workspace-path-unsafe", "workspace contains an unsafe path");
|
|
39
|
+
}
|
|
40
|
+
const parts = file.split("/");
|
|
41
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
42
|
+
throw new BehaviorWorkspaceError("workspace-path-unsafe", "workspace contains an unsafe path");
|
|
43
|
+
}
|
|
44
|
+
const absolute = resolve(root, file);
|
|
45
|
+
const fromRoot = relative(root, absolute);
|
|
46
|
+
if (!fromRoot || fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {
|
|
47
|
+
throw new BehaviorWorkspaceError("workspace-path-unsafe", "workspace path escapes the repository");
|
|
48
|
+
}
|
|
49
|
+
return file;
|
|
50
|
+
}
|
|
51
|
+
function nulList(buffer) {
|
|
52
|
+
return buffer.toString("utf8").split("\0").filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
export function captureBehaviorWorkspace(root, baseCommit, kind, env) {
|
|
55
|
+
const diffArgs = ["diff", "--binary", "--full-index", "--no-color", "--no-ext-diff", "--no-textconv"];
|
|
56
|
+
if (kind === "staged")
|
|
57
|
+
diffArgs.push("--cached");
|
|
58
|
+
diffArgs.push(baseCommit, "--", ...WORKSPACE_PATHS);
|
|
59
|
+
const patch = git(root, diffArgs, env);
|
|
60
|
+
const changedArgs = ["diff", "--name-only", "-z", "--diff-filter=ACMRD"];
|
|
61
|
+
if (kind === "staged")
|
|
62
|
+
changedArgs.push("--cached");
|
|
63
|
+
changedArgs.push(baseCommit, "--", ...WORKSPACE_PATHS);
|
|
64
|
+
const changed = nulList(git(root, changedArgs, env)).map((file) => safeRelative(root, file));
|
|
65
|
+
const untrackedNames = kind === "working"
|
|
66
|
+
? nulList(git(root, ["ls-files", "-z", "--others", "--exclude-standard", "--", ...WORKSPACE_PATHS], env))
|
|
67
|
+
: [];
|
|
68
|
+
if (untrackedNames.length > MAX_UNTRACKED_FILES) {
|
|
69
|
+
throw new BehaviorWorkspaceError("workspace-snapshot-too-large", `workspace has more than ${MAX_UNTRACKED_FILES} untracked files`);
|
|
70
|
+
}
|
|
71
|
+
let untrackedBytes = 0;
|
|
72
|
+
const untracked = untrackedNames.map((raw) => {
|
|
73
|
+
const file = safeRelative(root, raw);
|
|
74
|
+
const absolute = resolve(root, file);
|
|
75
|
+
const stat = lstatSync(absolute);
|
|
76
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
77
|
+
throw new BehaviorWorkspaceError("workspace-file-unsupported", `workspace untracked path is not a regular file: ${file}`);
|
|
78
|
+
}
|
|
79
|
+
untrackedBytes += stat.size;
|
|
80
|
+
if (untrackedBytes > MAX_UNTRACKED_BYTES) {
|
|
81
|
+
throw new BehaviorWorkspaceError("workspace-snapshot-too-large", "workspace untracked content exceeds the 8 MiB limit");
|
|
82
|
+
}
|
|
83
|
+
const content = readFileSync(absolute);
|
|
84
|
+
return { file, content_hash: bufferSha1(content), mode: stat.mode & 0o777, content };
|
|
85
|
+
});
|
|
86
|
+
const files = [...new Set([...changed, ...untracked.map((entry) => entry.file)])].sort();
|
|
87
|
+
if (files.length > 10_000) {
|
|
88
|
+
throw new BehaviorWorkspaceError("workspace-snapshot-too-large", "workspace has more than 10000 changed files");
|
|
89
|
+
}
|
|
90
|
+
const snapshotHash = canonicalHash({
|
|
91
|
+
kind,
|
|
92
|
+
base_commit: baseCommit,
|
|
93
|
+
patch_hash: bufferSha1(patch),
|
|
94
|
+
untracked: untracked.map(({ content: _content, ...entry }) => entry),
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
metadata: { kind, base_commit: baseCommit, snapshot_hash: snapshotHash, files },
|
|
98
|
+
patch,
|
|
99
|
+
untracked,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function applyBehaviorWorkspace(root, checkout, snapshot, env) {
|
|
103
|
+
if (snapshot.patch.length) {
|
|
104
|
+
const apply = spawnSync("git", ["-C", checkout, "apply", "--binary", "--whitespace=nowarn"], {
|
|
105
|
+
env,
|
|
106
|
+
input: snapshot.patch,
|
|
107
|
+
encoding: "buffer",
|
|
108
|
+
maxBuffer: MAX_PATCH_BYTES,
|
|
109
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
110
|
+
shell: false,
|
|
111
|
+
});
|
|
112
|
+
if (apply.error || apply.status !== 0) {
|
|
113
|
+
throw new BehaviorWorkspaceError("workspace-patch-apply-failed", "workspace patch could not be applied to the disposable checkout");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const entry of snapshot.untracked) {
|
|
117
|
+
const file = safeRelative(root, entry.file);
|
|
118
|
+
const destination = resolve(checkout, file);
|
|
119
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
120
|
+
writeFileSync(destination, entry.content);
|
|
121
|
+
chmodSync(destination, entry.mode);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=behaviorWorkspace.js.map
|