@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.
Files changed (58) hide show
  1. package/README.md +92 -1
  2. package/dist/cli/index.js +1222 -397
  3. package/dist/constitution/adapters.js +31 -14
  4. package/dist/constitution/behaviorEvaluator.js +20 -7
  5. package/dist/constitution/behaviorProof.js +3 -2
  6. package/dist/constitution/canonical.js +7 -1
  7. package/dist/constitution/card.js +7 -2
  8. package/dist/constitution/compiler.js +71 -1
  9. package/dist/constitution/correctionPolicyMaterializer.js +496 -0
  10. package/dist/constitution/delta.js +3 -2
  11. package/dist/constitution/evaluator.js +29 -3
  12. package/dist/constitution/experiment.js +6 -5
  13. package/dist/constitution/experimentRunner.js +43 -14
  14. package/dist/constitution/g2BehaviorCandidates.js +49 -26
  15. package/dist/constitution/g2BehaviorDependencies.js +203 -14
  16. package/dist/constitution/g2Candidates.js +1 -1
  17. package/dist/constitution/lifecycle.js +17 -0
  18. package/dist/constitution/plan.js +26 -9
  19. package/dist/constitution/replacementFreeGit.js +67 -0
  20. package/dist/constitution/replay.js +6 -0
  21. package/dist/constitution/replayCache.js +1 -1
  22. package/dist/constitution/replayWorker.js +1 -1
  23. package/dist/constitution/repository.js +141 -5
  24. package/dist/constitution/safeCheckout.js +75 -0
  25. package/dist/constitution/schema.js +30 -5
  26. package/dist/constitution/service.js +53 -10
  27. package/dist/constitution/sourceMutation.js +65 -12
  28. package/dist/constitution/staticGraphBaseline.js +44 -0
  29. package/dist/constitution/structural.js +60 -4
  30. package/dist/core/autoreview.js +1 -1
  31. package/dist/core/canonicalOrder.js +6 -0
  32. package/dist/core/conformance.js +68 -27
  33. package/dist/core/docscan.js +2 -1
  34. package/dist/core/escalations.js +11 -0
  35. package/dist/core/io.js +44 -9
  36. package/dist/core/overlaySafety.js +178 -0
  37. package/dist/core/paths.js +13 -2
  38. package/dist/core/safeRepoFile.js +74 -0
  39. package/dist/extractors/comments.js +6 -8
  40. package/dist/extractors/git.js +1631 -82
  41. package/dist/extractors/indexer.js +86 -47
  42. package/dist/extractors/repoSource.js +390 -0
  43. package/dist/integrations/ciAction.js +10 -2
  44. package/dist/integrations/gitignore.js +44 -5
  45. package/dist/integrations/mergeDriver.js +23 -5
  46. package/dist/integrations/sync.js +61 -5
  47. package/dist/integrations/team.js +666 -23
  48. package/dist/mcp/server.js +261 -34
  49. package/dist/store/db.js +57 -7
  50. package/dist/store/hunchStore.js +92 -11
  51. package/dist/store/jsonStore.js +350 -63
  52. package/dist/store/schema.js +27 -11
  53. package/dist/synthesis/provider.js +13 -4
  54. package/dist/synthesis/synthesize.js +56 -19
  55. package/dist/wiki/graph.js +5 -4
  56. package/dist/wiki/wiki.js +16 -10
  57. package/package.json +7 -2
  58. package/tooling/md1-benchmark.mjs +628 -0
@@ -1,10 +1,12 @@
1
- import { basename } from "node:path";
2
1
  import { shortHash } from "../core/ids.js";
3
- import { firstCommitForFile, headSha, revExists, revParse } from "../extractors/git.js";
2
+ import { stableRepositoryName } from "../extractors/git.js";
4
3
  import { canonicalHash, policySemanticHash } from "./canonical.js";
5
4
  import { policyCompositionBinding, policyProofHash } from "./composition.js";
6
5
  import { graphSnapshot, mutationOperatorForPolicy, selectedPolicyForComposition } from "./evaluator.js";
7
6
  import { createExecutableBehaviorProofPlan } from "./behaviorProof.js";
7
+ import { canonicalStaticGraphBaseline, isAncestorOrSame } from "./staticGraphBaseline.js";
8
+ import { replacementFreeExactCommit, replacementFreeFirstCommitForFile } from "./replacementFreeGit.js";
9
+ export { canonicalStaticGraphBaseline } from "./staticGraphBaseline.js";
8
10
  import { POLICY_EVALUATOR, MUTATION_ENGINE, ProofPlanSchema, } from "./schema.js";
9
11
  function clamp(value, fallback, min, max) {
10
12
  if (value == null || !Number.isFinite(value))
@@ -46,19 +48,26 @@ export function createProofPlan(store, root, repository, policy, opts = {}) {
46
48
  throw new Error("executable-behavior policies cannot have exception composition");
47
49
  return createExecutableBehaviorProofPlan(root, repository, policy, { now: opts.now, privateOnly: true });
48
50
  }
49
- const head = headSha(root);
50
- if (!head)
51
+ const repositoryHead = replacementFreeExactCommit(root, "HEAD");
52
+ if (!repositoryHead)
51
53
  throw new Error("proof planning needs a Git repository with a current HEAD");
54
+ const head = canonicalStaticGraphBaseline(root, repositoryHead);
52
55
  const composition = opts.composition ?? [];
53
56
  const parentHash = policySemanticHash(policy);
54
57
  const policyHash = policyProofHash(policy, composition);
55
58
  const compositionBinding = policyCompositionBinding(policy, composition);
56
59
  const corpus = repository.getCorpus(policy.id, opts);
60
+ // Proof plans are shared memory. A clone-local directory basename would mint
61
+ // different plan IDs for Architect, Developer, CI, and linked worktrees even
62
+ // when every one of them is looking at the same repository and policy. An
63
+ // existing immutable corpus remains the compatibility authority for artifacts
64
+ // created before stable repository identities were introduced.
65
+ const repositoryName = opts.repositoryName ?? corpus?.repository ?? stableRepositoryName(root);
57
66
  if (corpus) {
58
67
  if (corpus.policy_hash !== parentHash) {
59
68
  throw new Error(`proof corpus ${corpus.id} is stale for policy ${policy.id}; re-import it after the policy semantic change`);
60
69
  }
61
- if (corpus.repository !== basename(root) || corpus.data_class !== policy.data_class) {
70
+ if (corpus.repository !== repositoryName || corpus.data_class !== policy.data_class) {
62
71
  throw new Error(`proof corpus ${corpus.id} does not match repository/data class for policy ${policy.id}`);
63
72
  }
64
73
  }
@@ -73,11 +82,19 @@ export function createProofPlan(store, root, repository, policy, opts = {}) {
73
82
  .filter((ref) => ref.startsWith("dec_"))
74
83
  .map(readDecision)
75
84
  .find((record) => !!record);
76
- const policyCommit = firstCommitForFile(`.hunch/policies/${policy.id}.json`, root);
85
+ const policyCommit = replacementFreeFirstCommitForFile(root, `.hunch/policies/${policy.id}.json`);
77
86
  const sourceRef = sourceEvent?.commit ?? decision?.commit ?? (policyCommit || head);
78
- if (!revExists(sourceRef, root))
87
+ const rawSource = replacementFreeExactCommit(root, sourceRef);
88
+ if (!rawSource)
79
89
  throw new Error(`proof-plan source commit ${sourceRef} does not resolve in this repository`);
80
- const sourceCommit = revParse(sourceRef, root);
90
+ // Canonicalize the source independently of current HEAD. A policy introduced
91
+ // by a Hunch-only publication commit remains anchored to the indexed-code (or
92
+ // merge) boundary immediately before that publication, even after later code
93
+ // commits advance the current baseline.
94
+ const sourceCommit = canonicalStaticGraphBaseline(root, rawSource);
95
+ if (!isAncestorOrSame(root, sourceCommit, head)) {
96
+ throw new Error(`proof-plan source commit ${sourceCommit} is not an ancestor of canonical graph baseline ${head}`);
97
+ }
81
98
  const structural = events.find((event) => event.structural_delta && (event.kind === "bug_fix" || event.kind === "revert" || event.kind === "decision"));
82
99
  const structuralKnownBad = structural?.structural_delta
83
100
  ? [{
@@ -112,7 +129,7 @@ export function createProofPlan(store, root, repository, policy, opts = {}) {
112
129
  const body = {
113
130
  policy_id: policy.id,
114
131
  policy_candidate_hash: policyHash,
115
- repository: basename(root),
132
+ repository: repositoryName,
116
133
  data_class: policy.data_class,
117
134
  source_commit: sourceCommit,
118
135
  valid_from_commit: sourceCommit,
@@ -0,0 +1,67 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { foreignRepoEnv } from "../extractors/git.js";
3
+ /** Constitution proof identity is the repository's real object graph, never a
4
+ * clone-local `refs/replace/*` or legacy graft view. Keep this environment
5
+ * private to proof planning so every traversal and ancestry check agrees with
6
+ * replay, which uses the same Git invariant. */
7
+ export function replacementFreeGitEnvironment(source = process.env) {
8
+ return {
9
+ ...foreignRepoEnv(source),
10
+ GIT_NO_REPLACE_OBJECTS: "1",
11
+ };
12
+ }
13
+ function gitText(root, args, maxBuffer = 64 * 1024 * 1024) {
14
+ return execFileSync("git", ["-C", root, ...args], {
15
+ encoding: "utf8",
16
+ env: replacementFreeGitEnvironment(),
17
+ maxBuffer,
18
+ stdio: ["ignore", "pipe", "ignore"],
19
+ }).trim();
20
+ }
21
+ function gitTextSafe(root, args, maxBuffer) {
22
+ try {
23
+ return gitText(root, args, maxBuffer);
24
+ }
25
+ catch {
26
+ return "";
27
+ }
28
+ }
29
+ /** Resolve one commit-ish through the real object graph. */
30
+ export function replacementFreeExactCommit(root, ref) {
31
+ const oid = gitTextSafe(root, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).toLowerCase();
32
+ return /^[0-9a-f]{40,64}$/.test(oid) ? oid : null;
33
+ }
34
+ /** Files changed by an exact commit, ignoring local replacement objects. */
35
+ export function replacementFreeCommitFiles(root, commit) {
36
+ const out = gitTextSafe(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commit]);
37
+ return out ? out.split("\n").filter(Boolean) : [];
38
+ }
39
+ export function replacementFreeCommitMeta(root, commit) {
40
+ const raw = gitTextSafe(root, ["show", "-s", "--format=%H%x1f%h%x1f%s%x1f%b%x1f%an%x1f%aI", commit]);
41
+ if (!raw)
42
+ return null;
43
+ const [sha = "", shortSha = "", subject = "", body = "", author = "", date = ""] = raw.split("\x1f");
44
+ if (!/^[0-9a-f]{40,64}$/i.test(sha))
45
+ return null;
46
+ return { sha, shortSha, subject, body, author, date, files: replacementFreeCommitFiles(root, sha) };
47
+ }
48
+ /** Exact introducing commit for a Git-native policy record. */
49
+ export function replacementFreeFirstCommitForFile(root, file) {
50
+ const added = gitTextSafe(root, ["log", "--diff-filter=A", "--format=%H", "--", file])
51
+ .split("\n")
52
+ .find(Boolean);
53
+ if (added)
54
+ return added;
55
+ return gitTextSafe(root, ["log", "--reverse", "--format=%H", "--", file])
56
+ .split("\n")
57
+ .find(Boolean) ?? "";
58
+ }
59
+ export function replacementFreeIsAncestorOrSame(root, ancestor, descendant) {
60
+ if (ancestor === descendant)
61
+ return true;
62
+ return spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", ancestor, descendant], {
63
+ env: replacementFreeGitEnvironment(),
64
+ stdio: "ignore",
65
+ }).status === 0;
66
+ }
67
+ //# sourceMappingURL=replacementFreeGit.js.map
@@ -7,6 +7,7 @@ import { canonicalHash, proofEvaluationHash, proofPlanContentHash } from "./cano
7
7
  import { assertCompositionBinding, policyProofHash } from "./composition.js";
8
8
  import { evaluateCompositePolicyOnSnapshot, evaluatePolicyOnSnapshot } from "./evaluator.js";
9
9
  import { loadReplaySnapshot, putReplaySnapshot } from "./replayCache.js";
10
+ import { hasUnsafeCheckoutAttributes } from "./safeCheckout.js";
10
11
  import { POLICY_EVALUATOR, ProofPlanSchema, ReplayReceiptSchema, } from "./schema.js";
11
12
  const ZERO_SHA = "0".repeat(40);
12
13
  export const DEFAULT_REPLAY_WORKERS = 4;
@@ -26,6 +27,7 @@ export function replaySafeEnvironment(home, gitConfig) {
26
27
  HOME: home,
27
28
  GIT_CONFIG_GLOBAL: gitConfig,
28
29
  GIT_CONFIG_NOSYSTEM: "1",
30
+ GIT_NO_REPLACE_OBJECTS: "1",
29
31
  GIT_TERMINAL_PROMPT: "0",
30
32
  GIT_LFS_SKIP_SMUDGE: "1",
31
33
  HUNCH_PRIVATE_DIR: "",
@@ -204,6 +206,10 @@ export function replayProofPlan(root, policy, inputPlan, opts = {}) {
204
206
  outcomes.set(commit, { commit, error_code: "timeout" });
205
207
  continue;
206
208
  }
209
+ if (hasUnsafeCheckoutAttributes(root, commit, env, { allowDisabledLfs: true })) {
210
+ outcomes.set(commit, { commit, error_code: "unsafe-checkout-attributes" });
211
+ continue;
212
+ }
207
213
  const cached = loadReplaySnapshot(root, commit, plan.data_class);
208
214
  if (cached.status === "hit" && cached.snapshot) {
209
215
  cacheStats.hits++;
@@ -6,7 +6,7 @@ import { writeFileAtomic } from "../core/io.js";
6
6
  import { canonicalHash } from "./canonical.js";
7
7
  import { graphSnapshotFromRecords } from "./evaluator.js";
8
8
  import { DataClassSchema, POLICY_EVALUATOR } from "./schema.js";
9
- export const REPLAY_CACHE_ENGINE = { name: "hunch-tsjs-static-index", version: "3" };
9
+ export const REPLAY_CACHE_ENGINE = { name: "hunch-tsjs-static-index", version: "4" };
10
10
  const ReplayGraphCacheSchema = z.object({
11
11
  version: z.literal(1),
12
12
  engine: z.object({ name: z.string().min(1), version: z.string().min(1) }),
@@ -13,7 +13,7 @@ let message;
13
13
  try {
14
14
  store = new HunchStore(hunchPathsForDir(input.graph));
15
15
  store.json.ensureDirs();
16
- indexRepo(store, input.checkout, { churn: false });
16
+ indexRepo(store, input.checkout, { churn: false, requireComplete: true });
17
17
  message = {
18
18
  commit: input.commit,
19
19
  snapshot: graphSnapshot(store, input.root, { publicOnly: true, head: input.commit }),
@@ -1,8 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { writeFileAtomic } from "../core/io.js";
3
+ import { writeFileAtomic, writeFileAtomicIfAbsent } from "../core/io.js";
4
4
  import { shortHash } from "../core/ids.js";
5
- import { policySemanticHash, proofPlanContentHash } from "./canonical.js";
5
+ import { canonicalHash, policySemanticHash, proofPlanContentHash } from "./canonical.js";
6
6
  import { proofCorpusContentHash } from "./corpus.js";
7
7
  import { currentHistoryDispositions, historyDispositionContentHash, historyDispositionJudgmentHash } from "./disposition.js";
8
8
  import { assertCompositionBinding, compositionDescendants, policyProofHash } from "./composition.js";
@@ -218,9 +218,51 @@ export class PolicyRepository {
218
218
  writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
219
219
  return parsed;
220
220
  }
221
- putProof(proof, policyId) {
221
+ /** Publish a new policy lifecycle record without overwriting a concurrent
222
+ * writer. Used by automated proposal materializers so human authority always
223
+ * wins a race. */
224
+ putPolicyIfAbsent(policy, opts = {}) {
225
+ const parsed = PolicySpecSchema.parse(policy);
226
+ if (opts.private && opts.public)
227
+ throw new Error("choose only one policy home");
228
+ const home = opts.private ? "private" : opts.public ? "public" : parsed.data_class !== "public" || this.store.unified ? "private" : "public";
229
+ if (home === "public" && parsed.data_class !== "public") {
230
+ throw new Error(`refusing to write ${parsed.data_class} policy ${parsed.id} into the public home`);
231
+ }
232
+ const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
233
+ const existing = this.getPolicy(parsed.id, homeOpts);
234
+ const otherHome = this.getPolicy(parsed.id, home === "public" ? { privateOnly: true } : { publicOnly: true });
235
+ if (existing && otherHome)
236
+ throw new Error(`policy ${parsed.id} exists in both public and private homes`);
237
+ if (existing)
238
+ return { policy: existing, created: false };
239
+ if (otherHome)
240
+ throw new Error(`policy ${parsed.id} already exists in the ${home === "public" ? "private" : "public"} home`);
241
+ const dir = this.dir(home, "policies");
242
+ mkdirSync(dir, { recursive: true });
243
+ if (writeFileAtomicIfAbsent(join(dir, `${parsed.id}.json`), encode(parsed))) {
244
+ const racedOtherHome = this.getPolicy(parsed.id, home === "public" ? { privateOnly: true } : { publicOnly: true });
245
+ if (racedOtherHome)
246
+ throw new Error(`policy ${parsed.id} was published concurrently in both public and private homes`);
247
+ return { policy: parsed, created: true };
248
+ }
249
+ const winner = this.getPolicy(parsed.id, homeOpts);
250
+ if (!winner)
251
+ throw new Error(`policy ${parsed.id} appeared concurrently but could not be read`);
252
+ return { policy: winner, created: false };
253
+ }
254
+ putProof(proof, policyId, opts = {}) {
222
255
  const parsed = PolicyProofSchema.parse(proof);
223
- const home = this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
256
+ if (opts.private && opts.public)
257
+ throw new Error("choose only one proof home");
258
+ const home = opts.private
259
+ ? "private"
260
+ : opts.public
261
+ ? "public"
262
+ : this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
263
+ if (home === "public" && parsed.data_class !== "public") {
264
+ throw new Error(`refusing to write ${parsed.data_class} proof ${parsed.id} into the public home`);
265
+ }
224
266
  const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
225
267
  const policy = this.getPolicy(policyId, homeOpts);
226
268
  if (policy) {
@@ -240,6 +282,50 @@ export class PolicyRepository {
240
282
  writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
241
283
  return parsed;
242
284
  }
285
+ /** Publish an immutable proof without replacing a concurrent writer. */
286
+ putProofIfAbsent(proof, policyId, opts = {}) {
287
+ const parsed = PolicyProofSchema.parse(proof);
288
+ if (opts.private && opts.public)
289
+ throw new Error("choose only one proof home");
290
+ const home = opts.private
291
+ ? "private"
292
+ : opts.public
293
+ ? "public"
294
+ : this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
295
+ if (home === "public" && parsed.data_class !== "public") {
296
+ throw new Error(`refusing to write ${parsed.data_class} proof ${parsed.id} into the public home`);
297
+ }
298
+ const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
299
+ const policy = this.getPolicy(policyId, homeOpts);
300
+ if (policy) {
301
+ const composition = compositionDescendants(policy, this.listPolicies(homeOpts));
302
+ assertCompositionBinding(policy, composition, parsed.composition);
303
+ if (parsed.policy_hash !== policyProofHash(policy, composition))
304
+ throw new Error(`composite proof ${parsed.id} policy hash mismatch`);
305
+ if (composition.length) {
306
+ const plan = this.listPlans(homeOpts).find((candidate) => candidate.content_hash === parsed.plan_hash);
307
+ if (!plan || plan.policy_candidate_hash !== parsed.policy_hash)
308
+ throw new Error(`composite proof ${parsed.id} has no exact bound proof plan`);
309
+ assertCompositionBinding(policy, composition, plan.composition);
310
+ }
311
+ }
312
+ const existing = this.getProof(parsed.id, homeOpts);
313
+ if (existing) {
314
+ if (immutableProofHash(existing) !== immutableProofHash(parsed))
315
+ throw new Error(`proof ${parsed.id} already exists with different immutable content`);
316
+ return { proof: existing, created: false };
317
+ }
318
+ const dir = this.dir(home, "proofs");
319
+ mkdirSync(dir, { recursive: true });
320
+ if (writeFileAtomicIfAbsent(join(dir, `${parsed.id}.json`), encode(parsed)))
321
+ return { proof: parsed, created: true };
322
+ const winner = this.getProof(parsed.id, homeOpts);
323
+ if (!winner)
324
+ throw new Error(`proof ${parsed.id} appeared concurrently but could not be read`);
325
+ if (immutableProofHash(winner) !== immutableProofHash(parsed))
326
+ throw new Error(`proof ${parsed.id} appeared concurrently with different immutable content`);
327
+ return { proof: winner, created: false };
328
+ }
243
329
  putPlan(plan, policyId, opts = {}) {
244
330
  const parsed = validatePlan(plan);
245
331
  if (opts.private && opts.public)
@@ -249,6 +335,9 @@ export class PolicyRepository {
249
335
  : opts.public
250
336
  ? "public"
251
337
  : this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
338
+ if (home === "public" && parsed.data_class !== "public") {
339
+ throw new Error(`refusing to write ${parsed.data_class} proof plan ${parsed.id} into the public home`);
340
+ }
252
341
  const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
253
342
  const policy = this.getPolicy(policyId, homeOpts);
254
343
  if (policy) {
@@ -262,6 +351,44 @@ export class PolicyRepository {
262
351
  writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
263
352
  return parsed;
264
353
  }
354
+ /** Publish an immutable proof plan without replacing a concurrent writer. */
355
+ putPlanIfAbsent(plan, policyId, opts = {}) {
356
+ const parsed = validatePlan(plan);
357
+ if (opts.private && opts.public)
358
+ throw new Error("choose only one proof-plan home");
359
+ const home = opts.private
360
+ ? "private"
361
+ : opts.public
362
+ ? "public"
363
+ : this.homeOfPolicy(policyId) ?? (parsed.data_class === "public" && !this.store.unified ? "public" : "private");
364
+ if (home === "public" && parsed.data_class !== "public") {
365
+ throw new Error(`refusing to write ${parsed.data_class} proof plan ${parsed.id} into the public home`);
366
+ }
367
+ const homeOpts = home === "public" ? { publicOnly: true } : { privateOnly: true };
368
+ const policy = this.getPolicy(policyId, homeOpts);
369
+ if (policy) {
370
+ const composition = compositionDescendants(policy, this.listPolicies(homeOpts));
371
+ assertCompositionBinding(policy, composition, parsed.composition);
372
+ if (parsed.policy_candidate_hash !== policyProofHash(policy, composition))
373
+ throw new Error(`composite plan ${parsed.id} policy hash mismatch`);
374
+ }
375
+ const existing = this.getPlan(parsed.id, homeOpts);
376
+ if (existing) {
377
+ if (existing.content_hash !== parsed.content_hash)
378
+ throw new Error(`proof plan ${parsed.id} already exists with different immutable content`);
379
+ return { plan: existing, created: false };
380
+ }
381
+ const dir = this.dir(home, "plans");
382
+ mkdirSync(dir, { recursive: true });
383
+ if (writeFileAtomicIfAbsent(join(dir, `${parsed.id}.json`), encode(parsed)))
384
+ return { plan: parsed, created: true };
385
+ const winner = this.getPlan(parsed.id, homeOpts);
386
+ if (!winner)
387
+ throw new Error(`proof plan ${parsed.id} appeared concurrently but could not be read`);
388
+ if (winner.content_hash !== parsed.content_hash)
389
+ throw new Error(`proof plan ${parsed.id} appeared concurrently with different immutable content`);
390
+ return { plan: winner, created: false };
391
+ }
265
392
  putCorpus(corpus, policyId) {
266
393
  const parsed = validateCorpus(corpus);
267
394
  if (parsed.policy_id !== policyId)
@@ -280,7 +407,12 @@ export class PolicyRepository {
280
407
  }
281
408
  putEvidence(event, opts = {}) {
282
409
  const parsed = EvidenceEventSchema.parse(event);
283
- const home = opts.private || parsed.data_class !== "public" || this.store.unified ? "private" : "public";
410
+ if (opts.private && opts.public)
411
+ throw new Error("choose only one evidence home");
412
+ const home = opts.private ? "private" : opts.public ? "public" : parsed.data_class !== "public" || this.store.unified ? "private" : "public";
413
+ if (home === "public" && parsed.data_class !== "public") {
414
+ throw new Error(`refusing to write ${parsed.data_class} evidence ${parsed.id} into the public home`);
415
+ }
284
416
  const dir = this.dir(home, "evidence");
285
417
  mkdirSync(dir, { recursive: true });
286
418
  writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
@@ -395,6 +527,10 @@ export class PolicyRepository {
395
527
  return parsed;
396
528
  }
397
529
  }
530
+ function immutableProofHash(proof) {
531
+ const { generated_at: _generatedAt, ...payload } = proof;
532
+ return canonicalHash(payload);
533
+ }
398
534
  function validatePlan(raw) {
399
535
  const plan = ProofPlanSchema.parse(raw);
400
536
  const hash = proofPlanContentHash(plan);
@@ -0,0 +1,75 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ const CHECKOUT_TRANSFORM_ATTRIBUTES = ["filter", "working-tree-encoding", "ident", "eol", "text", "crlf"];
6
+ const MAX_ATTRIBUTE_BYTES = 64 * 1024 * 1024;
7
+ function nulFields(bytes) {
8
+ const fields = [];
9
+ let start = 0;
10
+ for (let end = bytes.indexOf(0, start); end !== -1; end = bytes.indexOf(0, start)) {
11
+ fields.push(bytes.subarray(start, end));
12
+ start = end + 1;
13
+ }
14
+ if (start < bytes.length)
15
+ fields.push(bytes.subarray(start));
16
+ return fields;
17
+ }
18
+ /** Inspect the exact target tree plus repository-local info attributes without
19
+ * checking it out. Any checkout transform could execute code or make the worktree
20
+ * bytes diverge from the raw blobs bound by receipts, so fail closed. Merge
21
+ * attributes are intentionally excluded: worktree materialization never invokes a
22
+ * merge driver. LFS is allowed only when callers explicitly disable its
23
+ * smudge/process hooks. */
24
+ export function hasUnsafeCheckoutAttributes(root, commit, env, opts = {}) {
25
+ const session = mkdtempSync(join(tmpdir(), "hunch-attr-index-"));
26
+ const index = join(session, "index");
27
+ const exactEnv = { ...env, GIT_INDEX_FILE: index, GIT_NO_REPLACE_OBJECTS: "1", GIT_ATTR_NOSYSTEM: "1" };
28
+ try {
29
+ execFileSync("git", ["-C", root, "read-tree", commit], {
30
+ env: exactEnv,
31
+ timeout: 10_000,
32
+ maxBuffer: MAX_ATTRIBUTE_BYTES,
33
+ stdio: ["ignore", "ignore", "ignore"],
34
+ });
35
+ const paths = execFileSync("git", ["-C", root, "ls-files", "-z"], {
36
+ env: exactEnv,
37
+ timeout: 10_000,
38
+ maxBuffer: MAX_ATTRIBUTE_BYTES,
39
+ encoding: "buffer",
40
+ stdio: ["ignore", "pipe", "ignore"],
41
+ });
42
+ if (!paths.length)
43
+ return false;
44
+ const raw = execFileSync("git", ["-C", root, "check-attr", "--cached", "-z", "--stdin", ...CHECKOUT_TRANSFORM_ATTRIBUTES], {
45
+ env: exactEnv,
46
+ input: paths,
47
+ timeout: 10_000,
48
+ maxBuffer: MAX_ATTRIBUTE_BYTES,
49
+ encoding: "buffer",
50
+ stdio: ["pipe", "pipe", "ignore"],
51
+ });
52
+ const fields = nulFields(raw);
53
+ if (fields.length % 3 !== 0)
54
+ return true;
55
+ for (let index = 0; index < fields.length; index += 3) {
56
+ const attribute = fields[index + 1].toString("utf8");
57
+ const value = fields[index + 2].toString("utf8");
58
+ if (!CHECKOUT_TRANSFORM_ATTRIBUTES.includes(attribute))
59
+ return true;
60
+ if (value === "unspecified" || value === "unset")
61
+ continue;
62
+ if (opts.allowDisabledLfs && attribute === "filter" && value === "lfs")
63
+ continue;
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ catch {
69
+ return true;
70
+ }
71
+ finally {
72
+ rmSync(session, { recursive: true, force: true });
73
+ }
74
+ }
75
+ //# sourceMappingURL=safeCheckout.js.map
@@ -4,6 +4,10 @@ export const POLICY_IR_VERSION = 1;
4
4
  export const POLICY_EVALUATOR = { name: "hunch-graph-policy", version: "1.3.0" };
5
5
  export const MUTATION_ENGINE = { name: "hunch-static-graph-controls", version: "5" };
6
6
  export const EXECUTABLE_BEHAVIOR_IR_VERSION = 2;
7
+ /** Source-gated correction policies deliberately use an IR unknown to pre-Matrix
8
+ * clients. Those clients then reject the artifact instead of loading it without
9
+ * understanding (and potentially bypassing) its activation gate. */
10
+ export const CORRECTION_POLICY_IR_VERSION = 3;
7
11
  export const BEHAVIOR_POLICY_EVALUATOR = { name: "hunch-executable-behavior", version: "1.0.0" };
8
12
  export const BEHAVIOR_MUTATION_ENGINE = { name: "hunch-behavior-controls", version: "1" };
9
13
  export const DataClassSchema = z.enum(["public", "private", "secret"]);
@@ -195,10 +199,20 @@ export const PolicyAuthoritySchema = z.object({
195
199
  event: z.string().min(1),
196
200
  at: z.string().datetime({ offset: true }),
197
201
  });
202
+ export const PolicyActivationGateSchema = z.object({
203
+ kind: z.literal("source_currentness"),
204
+ status: z.literal("blocked"),
205
+ reason: z.string().min(1),
206
+ }).strict();
198
207
  export const PolicySpecSchema = z.object({
199
208
  id: z.string().regex(/^pol_[a-f0-9]{10}$/),
200
209
  topic: z.string().min(1),
201
- ir_version: z.union([z.literal(POLICY_IR_VERSION), z.literal(EXECUTABLE_BEHAVIOR_IR_VERSION)]),
210
+ origin: z.enum(["generic", "correction_md1a"]).default("generic"),
211
+ ir_version: z.union([
212
+ z.literal(POLICY_IR_VERSION),
213
+ z.literal(EXECUTABLE_BEHAVIOR_IR_VERSION),
214
+ z.literal(CORRECTION_POLICY_IR_VERSION),
215
+ ]),
202
216
  revision: z.number().int().min(1),
203
217
  state: PolicyStateSchema,
204
218
  statement: z.string().min(1),
@@ -208,6 +222,7 @@ export const PolicySpecSchema = z.object({
208
222
  severity: z.enum(["advisory", "warning", "blocking"]).default("warning"),
209
223
  surfaces: z.array(z.enum(["pre_edit", "pre_commit", "ci", "mcp", "cli"])).default(["cli", "mcp"]),
210
224
  authority: PolicyAuthoritySchema.nullable().default(null),
225
+ activation_gate: PolicyActivationGateSchema.nullable().default(null),
211
226
  evidence: z.array(z.string()).default([]),
212
227
  proof: z.string().nullable().default(null),
213
228
  reversal_conditions: z.array(z.string()).default([]),
@@ -225,11 +240,21 @@ export const PolicySpecSchema = z.object({
225
240
  updated_at: z.string().datetime({ offset: true }),
226
241
  provenance: ProvenanceSchema,
227
242
  }).passthrough().superRefine((policy, context) => {
228
- if (policy.assertion.kind === "executable-behavior" && policy.ir_version !== EXECUTABLE_BEHAVIOR_IR_VERSION) {
229
- context.addIssue({ code: "custom", path: ["ir_version"], message: `executable-behavior requires Policy IR v${EXECUTABLE_BEHAVIOR_IR_VERSION}` });
243
+ if (policy.origin === "correction_md1a") {
244
+ if (policy.assertion.kind === "executable-behavior") {
245
+ context.addIssue({ code: "custom", path: ["assertion", "kind"], message: "source-gated correction policies require a graph assertion" });
246
+ }
247
+ if (policy.ir_version !== CORRECTION_POLICY_IR_VERSION) {
248
+ context.addIssue({ code: "custom", path: ["ir_version"], message: `source-gated correction policies require Policy IR v${CORRECTION_POLICY_IR_VERSION}` });
249
+ }
230
250
  }
231
- if (policy.assertion.kind !== "executable-behavior" && policy.ir_version !== POLICY_IR_VERSION) {
232
- context.addIssue({ code: "custom", path: ["ir_version"], message: `graph assertions require Policy IR v${POLICY_IR_VERSION}` });
251
+ else {
252
+ if (policy.assertion.kind === "executable-behavior" && policy.ir_version !== EXECUTABLE_BEHAVIOR_IR_VERSION) {
253
+ context.addIssue({ code: "custom", path: ["ir_version"], message: `executable-behavior requires Policy IR v${EXECUTABLE_BEHAVIOR_IR_VERSION}` });
254
+ }
255
+ if (policy.assertion.kind !== "executable-behavior" && policy.ir_version !== POLICY_IR_VERSION) {
256
+ context.addIssue({ code: "custom", path: ["ir_version"], message: `graph assertions require Policy IR v${POLICY_IR_VERSION}` });
257
+ }
233
258
  }
234
259
  });
235
260
  export const PolicyCompositionMemberSchema = z.object({