@davesheffer/hunch 1.6.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.
Files changed (65) hide show
  1. package/README.md +220 -0
  2. package/bench/constitution-exp03-v1.json +70 -0
  3. package/dist/cli/index.js +1278 -44
  4. package/dist/constitution/adapters.js +487 -0
  5. package/dist/constitution/behaviorAttestationBinding.js +17 -0
  6. package/dist/constitution/behaviorEvaluator.js +220 -0
  7. package/dist/constitution/behaviorProof.js +205 -0
  8. package/dist/constitution/behaviorWorkspace.js +124 -0
  9. package/dist/constitution/bootstrap.js +133 -0
  10. package/dist/constitution/canonical.js +51 -0
  11. package/dist/constitution/card.js +133 -0
  12. package/dist/constitution/compiler.js +176 -0
  13. package/dist/constitution/composition.js +101 -0
  14. package/dist/constitution/corpus.js +58 -0
  15. package/dist/constitution/delta.js +154 -0
  16. package/dist/constitution/disposition.js +141 -0
  17. package/dist/constitution/evaluator.js +435 -0
  18. package/dist/constitution/experiment.js +948 -0
  19. package/dist/constitution/experimentRunner.js +344 -0
  20. package/dist/constitution/g2.js +291 -0
  21. package/dist/constitution/g2BehaviorAttestation.js +209 -0
  22. package/dist/constitution/g2BehaviorCandidates.js +703 -0
  23. package/dist/constitution/g2BehaviorDependencies.js +379 -0
  24. package/dist/constitution/g2BehaviorMaterialization.js +171 -0
  25. package/dist/constitution/g2BehaviorPolicyMaterializer.js +241 -0
  26. package/dist/constitution/g2CandidateAttestation.js +179 -0
  27. package/dist/constitution/g2Candidates.js +195 -0
  28. package/dist/constitution/g2Drills.js +122 -0
  29. package/dist/constitution/g3.js +511 -0
  30. package/dist/constitution/g3Conformance.js +115 -0
  31. package/dist/constitution/lifecycle.js +189 -0
  32. package/dist/constitution/mutation.js +262 -0
  33. package/dist/constitution/nodeTestEvidence.js +47 -0
  34. package/dist/constitution/plan.js +172 -0
  35. package/dist/constitution/policyRuntime.js +8 -0
  36. package/dist/constitution/proof.js +166 -0
  37. package/dist/constitution/replay.js +361 -0
  38. package/dist/constitution/replayCache.js +89 -0
  39. package/dist/constitution/replayWorker.js +34 -0
  40. package/dist/constitution/repository.js +533 -0
  41. package/dist/constitution/schema.js +545 -0
  42. package/dist/constitution/scorecard.js +106 -0
  43. package/dist/constitution/service.js +1149 -0
  44. package/dist/constitution/shadow.js +235 -0
  45. package/dist/constitution/sourceMutation.js +316 -0
  46. package/dist/constitution/structural.js +601 -0
  47. package/dist/core/autoreview.js +27 -3
  48. package/dist/core/dupdetect.js +10 -3
  49. package/dist/core/events.js +61 -0
  50. package/dist/core/externalImports.js +24 -0
  51. package/dist/core/hookpolicy.js +3 -0
  52. package/dist/core/relativeImports.js +33 -0
  53. package/dist/core/stats.js +115 -0
  54. package/dist/extractors/git.js +81 -0
  55. package/dist/extractors/indexer.js +39 -38
  56. package/dist/extractors/nativeTreeSitter.js +108 -0
  57. package/dist/extractors/parse.js +5 -15
  58. package/dist/integrations/claudemd.js +8 -1
  59. package/dist/integrations/gitignore.js +8 -0
  60. package/dist/integrations/providers.js +32 -10
  61. package/dist/integrations/sync.js +16 -1
  62. package/dist/mcp/server.js +284 -0
  63. package/dist/synthesis/provider.js +145 -37
  64. package/dist/synthesis/synthesize.js +4 -4
  65. package/package.json +5 -1
@@ -0,0 +1,379 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
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";
5
+ import { shortHash } from "../core/ids.js";
6
+ import { canonicalHash, canonicalJson } from "./canonical.js";
7
+ import { replaySafeEnvironment } from "./replay.js";
8
+ const FULL_SHA = /^[a-f0-9]{40}$/;
9
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
10
+ const SNAPSHOT_VERSION = 1;
11
+ function sha256(value) {
12
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
13
+ }
14
+ function gitFile(root, commit, file) {
15
+ if (!FULL_SHA.test(commit))
16
+ throw new Error(`dependency snapshot commit ${commit} is not a full SHA`);
17
+ try {
18
+ return execFileSync("git", ["-C", root, "show", `${commit}:${file}`], {
19
+ encoding: "utf8",
20
+ maxBuffer: 20 * 1024 * 1024,
21
+ stdio: ["ignore", "pipe", "ignore"],
22
+ });
23
+ }
24
+ catch {
25
+ throw new Error(`dependency snapshot requires ${file} at ${commit}`);
26
+ }
27
+ }
28
+ function parseObject(raw, label) {
29
+ let value;
30
+ try {
31
+ value = JSON.parse(raw);
32
+ }
33
+ catch {
34
+ throw new Error(`dependency snapshot ${label} is not valid JSON`);
35
+ }
36
+ if (!value || typeof value !== "object" || Array.isArray(value))
37
+ throw new Error(`dependency snapshot ${label} must be a JSON object`);
38
+ return value;
39
+ }
40
+ function dependencyProjection(pkg) {
41
+ return {
42
+ dependencies: pkg.dependencies ?? {},
43
+ devDependencies: pkg.devDependencies ?? {},
44
+ optionalDependencies: pkg.optionalDependencies ?? {},
45
+ peerDependencies: pkg.peerDependencies ?? {},
46
+ peerDependenciesMeta: pkg.peerDependenciesMeta ?? {},
47
+ engines: pkg.engines ?? {},
48
+ };
49
+ }
50
+ function lockedPackageNames(lock) {
51
+ const names = new Set();
52
+ const packages = lock.packages;
53
+ if (!packages || typeof packages !== "object" || Array.isArray(packages))
54
+ return names;
55
+ for (const key of Object.keys(packages)) {
56
+ if (!key.startsWith("node_modules/"))
57
+ continue;
58
+ const name = key.slice("node_modules/".length);
59
+ if (name && !name.includes("/node_modules/"))
60
+ names.add(name);
61
+ }
62
+ return names;
63
+ }
64
+ function normalizeAllowlist(values, lock) {
65
+ const result = [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
66
+ const locked = lockedPackageNames(lock);
67
+ for (const name of result) {
68
+ if (!PACKAGE_NAME.test(name))
69
+ throw new Error(`invalid dependency install-script package ${JSON.stringify(name)}`);
70
+ if (!locked.has(name))
71
+ throw new Error(`dependency install-script package ${name} is absent from the exact lockfile`);
72
+ }
73
+ return result;
74
+ }
75
+ function npmExecutable() {
76
+ return process.platform === "win32" ? "npm.cmd" : "npm";
77
+ }
78
+ // npm on Windows is a .cmd shim, which Node's CVE-2024-27980 hardening refuses
79
+ // to spawn with shell:false (EINVAL). Mirror src/synthesis/provider.ts: route
80
+ // through cmd.exe as ONE trusted line — every argv here is a fixed flag or a
81
+ // lockfile-validated package name, so nothing can be shell-interpreted. POSIX
82
+ // keeps shell:false with argv as-is.
83
+ function spawnNpm(args, opts) {
84
+ return process.platform === "win32"
85
+ ? spawnSync([npmExecutable(), ...args].join(" "), { ...opts, encoding: "utf8", shell: true, windowsHide: true })
86
+ : spawnSync(npmExecutable(), args, { ...opts, encoding: "utf8", shell: false });
87
+ }
88
+ function buildEnvironment(home) {
89
+ const env = replaySafeEnvironment(home, join(home, "global.gitconfig"));
90
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy", "npm_config_registry"]) {
91
+ if (process.env[key])
92
+ env[key] = process.env[key];
93
+ }
94
+ return env;
95
+ }
96
+ function runtimeIdentity(env) {
97
+ const result = spawnNpm(["--version"], {
98
+ env,
99
+ timeout: 10_000,
100
+ stdio: ["ignore", "pipe", "ignore"],
101
+ });
102
+ if (result.status !== 0 || !result.stdout.trim())
103
+ throw new Error("dependency snapshot could not identify npm");
104
+ return { node: process.version, npm: result.stdout.trim(), platform: process.platform, arch: process.arch };
105
+ }
106
+ function snapshotInput(root, commit, allowInstallScripts, env) {
107
+ const packageJson = gitFile(root, commit, "package.json");
108
+ const packageLock = gitFile(root, commit, "package-lock.json");
109
+ const pkg = parseObject(packageJson, "package.json");
110
+ const lock = parseObject(packageLock, "package-lock.json");
111
+ if (lock.lockfileVersion !== 2 && lock.lockfileVersion !== 3) {
112
+ throw new Error(`dependency snapshot requires npm lockfileVersion 2 or 3 at ${commit}`);
113
+ }
114
+ const normalizedAllowlist = normalizeAllowlist(allowInstallScripts, lock);
115
+ const sanitizedPackage = { ...pkg, scripts: {} };
116
+ const runtime = runtimeIdentity(env);
117
+ const body = {
118
+ package_json_hash: sha256(packageJson),
119
+ sanitized_package_json_hash: sha256(canonicalJson(sanitizedPackage)),
120
+ package_lock_hash: sha256(packageLock),
121
+ dependency_projection_hash: canonicalHash(dependencyProjection(pkg)),
122
+ runtime,
123
+ allow_install_scripts: normalizedAllowlist,
124
+ format_version: SNAPSHOT_VERSION,
125
+ };
126
+ return {
127
+ packageJson,
128
+ packageLock,
129
+ sanitizedPackage,
130
+ inputHash: canonicalHash(body),
131
+ packageJsonHash: body.package_json_hash,
132
+ sanitizedPackageJsonHash: body.sanitized_package_json_hash,
133
+ packageLockHash: body.package_lock_hash,
134
+ dependencyProjectionHash: body.dependency_projection_hash,
135
+ runtime,
136
+ allowInstallScripts: normalizedAllowlist,
137
+ };
138
+ }
139
+ function nativeInventory(nodeModules) {
140
+ const files = [];
141
+ const walk = (dir, relative) => {
142
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
143
+ const absolute = join(dir, entry.name);
144
+ const next = relative ? `${relative}/${entry.name}` : entry.name;
145
+ if (entry.isSymbolicLink())
146
+ continue;
147
+ if (entry.isDirectory())
148
+ walk(absolute, next);
149
+ else if (entry.isFile() && entry.name.endsWith(".node"))
150
+ files.push({ file: next, sha256: sha256(readFileSync(absolute)) });
151
+ }
152
+ };
153
+ walk(nodeModules, "");
154
+ return files;
155
+ }
156
+ function manifestHash(snapshot) {
157
+ const { id: _id, content_hash: _contentHash, ...body } = snapshot;
158
+ return canonicalHash(body);
159
+ }
160
+ function readSnapshot(dir) {
161
+ const file = join(dir, "manifest.json");
162
+ if (!existsSync(file))
163
+ return null;
164
+ try {
165
+ const value = JSON.parse(readFileSync(file, "utf8"));
166
+ if (value.format_version !== SNAPSHOT_VERSION || value.content_hash !== manifestHash(value))
167
+ return null;
168
+ if (value.id !== `g2deps_${shortHash(value.content_hash)}` || basename(dir) !== value.id)
169
+ return null;
170
+ const nodeModules = join(dir, "node_modules");
171
+ const installedLock = join(nodeModules, ".package-lock.json");
172
+ if (!existsSync(nodeModules) || !lstatSync(nodeModules).isDirectory())
173
+ return null;
174
+ const installedLockHash = existsSync(installedLock) ? sha256(readFileSync(installedLock)) : sha256("");
175
+ if (installedLockHash !== value.installed_lock_hash)
176
+ return null;
177
+ if (canonicalJson(nativeInventory(nodeModules)) !== canonicalJson(value.native_binaries))
178
+ return null;
179
+ return value;
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ function snapshotDirs(base) {
186
+ if (!existsSync(base))
187
+ return [];
188
+ return readdirSync(base, { withFileTypes: true })
189
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("g2deps_"))
190
+ .map((entry) => join(base, entry.name))
191
+ .sort();
192
+ }
193
+ function findSnapshot(base, inputHash) {
194
+ for (const dir of snapshotDirs(base)) {
195
+ const snapshot = readSnapshot(dir);
196
+ if (snapshot?.input_hash === inputHash)
197
+ return { snapshot, dir };
198
+ }
199
+ return null;
200
+ }
201
+ function runNpm(args, cwd, env, timeoutMs) {
202
+ const result = spawnNpm(args, {
203
+ cwd,
204
+ env,
205
+ timeout: timeoutMs,
206
+ maxBuffer: 10 * 1024 * 1024,
207
+ stdio: ["ignore", "pipe", "pipe"],
208
+ });
209
+ if (result.error) {
210
+ const code = result.error.code === "ETIMEDOUT" ? "timed out" : "failed to start";
211
+ throw new Error(`dependency snapshot npm ${code}`);
212
+ }
213
+ if (result.status !== 0) {
214
+ const detail = `${result.stderr ?? ""}\n${result.stdout ?? ""}`.trim().split("\n").slice(-8).join("\n")
215
+ .replace(/(_authToken=)[^\s]+/gi, "$1[redacted]")
216
+ .replace(/(https?:\/\/)[^/@\s]+@/gi, "$1[redacted]@");
217
+ throw new Error(`dependency snapshot npm exited ${result.status}${detail ? `:\n${detail}` : ""}`);
218
+ }
219
+ }
220
+ function buildSnapshot(base, input, env, timeoutMs) {
221
+ const existing = findSnapshot(base, input.inputHash);
222
+ if (existing)
223
+ return existing.snapshot;
224
+ const work = mkdtempSync(join(base, ".build-"));
225
+ try {
226
+ writeFileSync(join(work, "package.json"), canonicalJson(input.sanitizedPackage));
227
+ writeFileSync(join(work, "package-lock.json"), input.packageLock);
228
+ runNpm(["ci", "--ignore-scripts", "--no-audit", "--no-fund"], work, env, timeoutMs);
229
+ if (input.allowInstallScripts.length) {
230
+ runNpm(["rebuild", ...input.allowInstallScripts, "--foreground-scripts", "--no-audit", "--no-fund"], work, env, timeoutMs);
231
+ }
232
+ const nodeModules = join(work, "node_modules");
233
+ const installedLock = join(nodeModules, ".package-lock.json");
234
+ mkdirSync(nodeModules, { recursive: true });
235
+ const body = {
236
+ input_hash: input.inputHash,
237
+ package_json_hash: input.packageJsonHash,
238
+ sanitized_package_json_hash: input.sanitizedPackageJsonHash,
239
+ package_lock_hash: input.packageLockHash,
240
+ dependency_projection_hash: input.dependencyProjectionHash,
241
+ runtime: input.runtime,
242
+ allow_install_scripts: input.allowInstallScripts,
243
+ installed_lock_hash: existsSync(installedLock) ? sha256(readFileSync(installedLock)) : sha256(""),
244
+ native_binaries: nativeInventory(nodeModules),
245
+ format_version: SNAPSHOT_VERSION,
246
+ data_class: "private",
247
+ authority: "none",
248
+ effects: "cache_only",
249
+ writes: "cache_only",
250
+ };
251
+ const contentHash = canonicalHash(body);
252
+ const snapshot = {
253
+ id: `g2deps_${shortHash(contentHash)}`,
254
+ content_hash: contentHash,
255
+ ...body,
256
+ };
257
+ writeFileSync(join(work, "manifest.json"), canonicalJson(snapshot));
258
+ const finalDir = join(base, snapshot.id);
259
+ if (existsSync(finalDir)) {
260
+ const collision = readSnapshot(finalDir);
261
+ if (!collision || collision.content_hash !== snapshot.content_hash)
262
+ throw new Error(`dependency snapshot collision at ${snapshot.id}`);
263
+ return collision;
264
+ }
265
+ try {
266
+ renameSync(work, finalDir);
267
+ }
268
+ catch (error) {
269
+ if (!existsSync(finalDir))
270
+ throw error;
271
+ const concurrent = readSnapshot(finalDir);
272
+ if (!concurrent || concurrent.content_hash !== snapshot.content_hash) {
273
+ throw new Error(`dependency snapshot collision at ${snapshot.id}`);
274
+ }
275
+ return concurrent;
276
+ }
277
+ const validated = readSnapshot(finalDir);
278
+ if (!validated)
279
+ throw new Error(`dependency snapshot ${snapshot.id} failed post-build validation`);
280
+ return validated;
281
+ }
282
+ finally {
283
+ rmSync(work, { recursive: true, force: true });
284
+ }
285
+ }
286
+ export function dependencySnapshotById(root, id) {
287
+ if (!/^g2deps_[a-f0-9]{10}$/.test(id))
288
+ return null;
289
+ const dir = join(root, ".hunch-cache", "behavior-deps", id);
290
+ const snapshot = readSnapshot(dir);
291
+ return snapshot ? { snapshot, nodeModules: join(dir, "node_modules") } : null;
292
+ }
293
+ export function dependencySnapshotForCommit(root, commit, allowedIds) {
294
+ const base = join(root, ".hunch-cache", "behavior-deps");
295
+ if (!existsSync(base))
296
+ return null;
297
+ const env = buildEnvironment(base);
298
+ let packageJson;
299
+ let packageLock;
300
+ try {
301
+ packageJson = gitFile(root, commit, "package.json");
302
+ packageLock = gitFile(root, commit, "package-lock.json");
303
+ }
304
+ catch {
305
+ return null;
306
+ }
307
+ const runtime = runtimeIdentity(env);
308
+ const packageJsonHash = sha256(packageJson);
309
+ const packageLockHash = sha256(packageLock);
310
+ const matches = [];
311
+ for (const dir of snapshotDirs(base)) {
312
+ const snapshot = readSnapshot(dir);
313
+ if (snapshot
314
+ && (!allowedIds || allowedIds.includes(snapshot.id))
315
+ && snapshot.package_json_hash === packageJsonHash
316
+ && snapshot.package_lock_hash === packageLockHash
317
+ && canonicalJson(snapshot.runtime) === canonicalJson(runtime)) {
318
+ matches.push({ snapshot, nodeModules: join(dir, "node_modules") });
319
+ }
320
+ }
321
+ return matches.length === 1 ? matches[0] : null;
322
+ }
323
+ export function provisionG2BehaviorDependencySnapshotsForCommits(root, commits, allowInstallScripts = [], timeoutMs = 300_000) {
324
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 900_000) {
325
+ throw new Error("dependency snapshot timeoutMs must be a positive integer no greater than 900000");
326
+ }
327
+ const normalizedCommits = [...new Set(commits)].sort();
328
+ if (!normalizedCommits.length || normalizedCommits.some((commit) => !FULL_SHA.test(commit))) {
329
+ throw new Error("dependency snapshots require at least one full lowercase 40-character commit SHA");
330
+ }
331
+ const base = join(root, ".hunch-cache", "behavior-deps");
332
+ mkdirSync(base, { recursive: true });
333
+ const home = mkdtempSync(join(base, ".npm-home-"));
334
+ writeFileSync(join(home, "global.gitconfig"), "");
335
+ const env = buildEnvironment(home);
336
+ try {
337
+ const byInput = new Map();
338
+ const mapped = normalizedCommits.map((commit) => {
339
+ const input = snapshotInput(root, commit, allowInstallScripts, env);
340
+ const snapshot = byInput.get(input.inputHash) ?? buildSnapshot(base, input, env, timeoutMs);
341
+ byInput.set(input.inputHash, snapshot);
342
+ return { commit, dependency_snapshot_id: snapshot.id };
343
+ });
344
+ return {
345
+ snapshots: [...new Map([...byInput.values()].map((snapshot) => [snapshot.id, snapshot])).values()]
346
+ .sort((left, right) => left.id.localeCompare(right.id)),
347
+ commits: mapped,
348
+ };
349
+ }
350
+ finally {
351
+ rmSync(home, { recursive: true, force: true });
352
+ }
353
+ }
354
+ export function provisionG2BehaviorDependencySnapshots(root, report, candidate, allowInstallScripts = [], timeoutMs = 300_000) {
355
+ const provisioned = provisionG2BehaviorDependencySnapshotsForCommits(root, [
356
+ candidate.proposed_corpus.known_bad.ref,
357
+ candidate.proposed_corpus.known_good.ref,
358
+ ], allowInstallScripts, timeoutMs);
359
+ const bad = provisioned.commits.find((entry) => entry.commit === candidate.proposed_corpus.known_bad.ref);
360
+ const good = provisioned.commits.find((entry) => entry.commit === candidate.proposed_corpus.known_good.ref);
361
+ const body = {
362
+ candidate_id: candidate.id,
363
+ candidate_hash: canonicalHash(candidate),
364
+ review_hash: report.content_hash,
365
+ snapshots: provisioned.snapshots,
366
+ legs: {
367
+ known_bad: { commit: candidate.proposed_corpus.known_bad.ref, dependency_snapshot_id: bad.dependency_snapshot_id },
368
+ known_good: { commit: candidate.proposed_corpus.known_good.ref, dependency_snapshot_id: good.dependency_snapshot_id },
369
+ },
370
+ allow_install_scripts: [...new Set(allowInstallScripts.map((value) => value.trim()).filter(Boolean))].sort(),
371
+ data_class: "private",
372
+ authority: "none",
373
+ effects: "cache_only",
374
+ writes: "cache_only",
375
+ };
376
+ const contentHash = canonicalHash(body);
377
+ return { id: `g2depsreceipt_${shortHash(contentHash)}`, content_hash: contentHash, ...body };
378
+ }
379
+ //# sourceMappingURL=g2BehaviorDependencies.js.map
@@ -0,0 +1,171 @@
1
+ import { z } from "zod";
2
+ import { shortHash } from "../core/ids.js";
3
+ import { canonicalHash } from "./canonical.js";
4
+ import { g2BehaviorCandidateHash, g2BehaviorReviewContentHash, } from "./g2BehaviorCandidates.js";
5
+ import { g2BehaviorAttestationContentHash, } from "./g2BehaviorAttestation.js";
6
+ import { BEHAVIOR_POLICY_EVALUATOR, EXECUTABLE_BEHAVIOR_IR_VERSION } from "./schema.js";
7
+ const HASH = /^sha1:[a-f0-9]{40}$/;
8
+ const SUPPORTED_ASSERTION_KINDS = ["executable-behavior", "exists", "must-pass-through", "not-reaches", "reaches"];
9
+ const SourceReviewSchema = z.object({
10
+ id: z.string().regex(/^g2behaviorcandidates_[a-f0-9]{10}$/),
11
+ content_hash: z.string().regex(HASH),
12
+ structural_review_hash: z.string().regex(HASH),
13
+ grounding_mode: z.literal("human_decision_plus_added_test").optional(),
14
+ source_decision_id: z.string().regex(/^dec_[A-Za-z0-9_-]+$/).optional(),
15
+ source_grounding_hash: z.string().regex(HASH).optional(),
16
+ }).strict();
17
+ const MaterializationItemSchema = z.object({
18
+ candidate_id: z.string().regex(/^g2behavior_[a-f0-9]{10}$/),
19
+ candidate_hash: z.string().regex(HASH),
20
+ attestation_id: z.string().regex(/^g2behaviorattest_[a-f0-9]{10}$/),
21
+ attestation_hash: z.string().regex(HASH),
22
+ attested_review_hash: z.string().regex(HASH),
23
+ replay_id: z.string().regex(/^g2behaviorreplay_[a-f0-9]{10}$/),
24
+ replay_hash: z.string().regex(HASH),
25
+ dependency_snapshot_ids: z.array(z.string().regex(/^g2deps_[a-f0-9]{10}$/)).min(1).max(2),
26
+ commit: z.string().regex(/^[a-f0-9]{40}$/),
27
+ test: z.object({
28
+ file: z.string().min(1),
29
+ name: z.string().min(1),
30
+ source_hash: z.string().regex(HASH),
31
+ }).strict(),
32
+ durable_meaning: z.string().min(1),
33
+ status: z.literal("ready_for_materialization"),
34
+ reason: z.string().min(1),
35
+ required_capability: z.object({
36
+ assertion: z.literal("executable-behavior"),
37
+ current_baseline: z.literal("required"),
38
+ known_bad: z.literal("must_fail_behaviorally"),
39
+ known_good: z.literal("must_pass"),
40
+ mutation_controls: z.literal("required"),
41
+ }).strict(),
42
+ }).strict();
43
+ export const G2BehaviorMaterializationAssessmentSchema = z.object({
44
+ id: z.string().regex(/^g2behaviormaterialization_[a-f0-9]{10}$/),
45
+ content_hash: z.string().regex(HASH),
46
+ source_review: SourceReviewSchema,
47
+ policy_ir_version: z.literal(EXECUTABLE_BEHAVIOR_IR_VERSION),
48
+ evaluator: z.object({
49
+ name: z.literal(BEHAVIOR_POLICY_EVALUATOR.name),
50
+ version: z.literal(BEHAVIOR_POLICY_EVALUATOR.version),
51
+ }).strict(),
52
+ supported_assertion_kinds: z.array(z.enum(SUPPORTED_ASSERTION_KINDS)).length(SUPPORTED_ASSERTION_KINDS.length),
53
+ selected_attestations: z.number().int().min(1),
54
+ materialized_policies: z.literal(0),
55
+ readiness: z.literal("ready_for_materialization"),
56
+ items: z.array(MaterializationItemSchema).min(1),
57
+ outputs: z.object({
58
+ policies: z.array(z.never()).length(0),
59
+ corpora: z.array(z.never()).length(0),
60
+ plans: z.array(z.never()).length(0),
61
+ proofs: z.array(z.never()).length(0),
62
+ }).strict(),
63
+ limitations: z.array(z.string().min(1)).min(1),
64
+ data_class: z.literal("private"),
65
+ authority: z.literal("none"),
66
+ effects: z.literal("assessment_only"),
67
+ writes: z.literal("none"),
68
+ proof_status: z.literal("not_run"),
69
+ activation: z.literal("separate_human_action_required"),
70
+ }).strict();
71
+ export function g2BehaviorMaterializationContentHash(assessment) {
72
+ const { id: _id, content_hash: _contentHash, ...body } = assessment;
73
+ return canonicalHash(body);
74
+ }
75
+ const readyReason = `The selected receipt is expressible by Policy IR v${EXECUTABLE_BEHAVIOR_IR_VERSION} through ${BEHAVIOR_POLICY_EVALUATOR.name}@${BEHAVIOR_POLICY_EVALUATOR.version}; materialization must still produce exact current, known-bad, known-good, and mutation-control receipts before proposal.`;
76
+ /**
77
+ * Assess exact selected behavior receipts against the currently implemented
78
+ * Policy IR. This is intentionally fail-closed: it never substitutes a static
79
+ * symbol or edge proxy for behavior that the evaluator cannot execute.
80
+ */
81
+ export function assessG2BehaviorMaterialization(report, currentAttestations) {
82
+ if (report.content_hash !== g2BehaviorReviewContentHash(report)
83
+ || report.id !== `g2behaviorcandidates_${shortHash(report.content_hash)}`) {
84
+ throw new Error(`G2 behavior candidate review ${report.id} content hash mismatch`);
85
+ }
86
+ if (report.has_more)
87
+ throw new Error("G2 behavior materialization requires a complete, untruncated behavior review");
88
+ if (report.unreviewed_candidates !== 0)
89
+ throw new Error("G2 behavior materialization requires every behavior candidate to have a current human disposition");
90
+ const candidateHashes = new Map(report.items.map((candidate) => [candidate.id, g2BehaviorCandidateHash(candidate)]));
91
+ const selected = currentAttestations
92
+ .filter((attestation) => attestation.disposition === "selected"
93
+ && candidateHashes.get(attestation.candidate_id) === attestation.candidate_hash)
94
+ .sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
95
+ if (!selected.length)
96
+ throw new Error("G2 behavior materialization requires at least one current selected attestation");
97
+ if (report.selected_candidates !== selected.length) {
98
+ throw new Error("G2 behavior materialization selected-attestation count does not match the exact current review");
99
+ }
100
+ const items = selected.map((attestation) => {
101
+ if (attestation.content_hash !== g2BehaviorAttestationContentHash(attestation)
102
+ || attestation.id !== `g2behaviorattest_${shortHash(attestation.content_hash)}`) {
103
+ throw new Error(`G2 behavior attestation ${attestation.id} content hash mismatch`);
104
+ }
105
+ const candidate = report.items.find((item) => item.id === attestation.candidate_id);
106
+ if (!candidate || g2BehaviorCandidateHash(candidate) !== attestation.candidate_hash) {
107
+ throw new Error(`G2 behavior attestation ${attestation.id} does not bind a candidate in the exact current review`);
108
+ }
109
+ if (candidate.human_review?.id !== attestation.id || candidate.human_review.disposition !== "selected") {
110
+ throw new Error(`G2 behavior attestation ${attestation.id} is not the current selected disposition projected by the review`);
111
+ }
112
+ return {
113
+ candidate_id: candidate.id,
114
+ candidate_hash: attestation.candidate_hash,
115
+ attestation_id: attestation.id,
116
+ attestation_hash: attestation.content_hash,
117
+ attested_review_hash: attestation.review_hash,
118
+ replay_id: attestation.replay_id,
119
+ replay_hash: attestation.replay_hash,
120
+ dependency_snapshot_ids: attestation.dependency_snapshot_ids,
121
+ commit: attestation.commit,
122
+ test: candidate.test,
123
+ durable_meaning: attestation.reason,
124
+ status: "ready_for_materialization",
125
+ reason: readyReason,
126
+ required_capability: {
127
+ assertion: "executable-behavior",
128
+ current_baseline: "required",
129
+ known_bad: "must_fail_behaviorally",
130
+ known_good: "must_pass",
131
+ mutation_controls: "required",
132
+ },
133
+ };
134
+ });
135
+ const body = {
136
+ source_review: {
137
+ id: report.id,
138
+ content_hash: report.content_hash,
139
+ structural_review_hash: report.structural_review_hash,
140
+ ...(report.grounding_mode ? { grounding_mode: report.grounding_mode } : {}),
141
+ ...(report.source_decision_id ? { source_decision_id: report.source_decision_id } : {}),
142
+ ...(report.source_grounding_hash ? { source_grounding_hash: report.source_grounding_hash } : {}),
143
+ },
144
+ policy_ir_version: EXECUTABLE_BEHAVIOR_IR_VERSION,
145
+ evaluator: { ...BEHAVIOR_POLICY_EVALUATOR },
146
+ supported_assertion_kinds: [...SUPPORTED_ASSERTION_KINDS],
147
+ selected_attestations: items.length,
148
+ materialized_policies: 0,
149
+ readiness: "ready_for_materialization",
150
+ items,
151
+ outputs: { policies: [], corpora: [], plans: [], proofs: [] },
152
+ limitations: [
153
+ "Assessment alone creates no PolicySpec, corpus, proof plan, proof, authority, warning, or block.",
154
+ "Executable materialization must independently prove current, known-bad, known-good, and mutation-control behavior before proposal.",
155
+ "Committed HEAD remains the proof baseline; advisory delivery may separately evaluate a content-addressed staged or working snapshot.",
156
+ ],
157
+ data_class: "private",
158
+ authority: "none",
159
+ effects: "assessment_only",
160
+ writes: "none",
161
+ proof_status: "not_run",
162
+ activation: "separate_human_action_required",
163
+ };
164
+ const contentHash = canonicalHash(body);
165
+ return G2BehaviorMaterializationAssessmentSchema.parse({
166
+ id: `g2behaviormaterialization_${shortHash(contentHash)}`,
167
+ content_hash: contentHash,
168
+ ...body,
169
+ });
170
+ }
171
+ //# sourceMappingURL=g2BehaviorMaterialization.js.map