@davesheffer/hunch 1.8.2 → 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 (59) hide show
  1. package/README.md +96 -1
  2. package/dist/cli/index.js +1238 -396
  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 +96 -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 +74 -14
  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 +15 -3
  58. package/tooling/competitive-watch.mjs +108 -0
  59. package/tooling/md1-benchmark.mjs +628 -0
@@ -4,9 +4,11 @@ import { basename, resolve } from "node:path";
4
4
  import { shortHash } from "../core/ids.js";
5
5
  import { parseDocAnchors } from "../core/docanchors.js";
6
6
  import { toPosixTarget } from "../core/paths.js";
7
- import { commitMeta, revExists, revParse } from "../extractors/git.js";
7
+ import { isHumanConfirmed } from "../core/strictgate.js";
8
+ import { stableRepositoryName } from "../extractors/git.js";
8
9
  import { durationCutoff } from "./bootstrap.js";
9
10
  import { canonicalHash } from "./canonical.js";
11
+ import { replacementFreeCommitMeta, replacementFreeExactCommit, replacementFreeGitEnvironment, } from "./replacementFreeGit.js";
10
12
  import { EvidenceEventSchema, EvidenceImportSchema, } from "./schema.js";
11
13
  const MAX_INSTRUCTION_FILES = 64;
12
14
  const MAX_INSTRUCTION_FILE_BYTES = 512 * 1024;
@@ -55,6 +57,7 @@ function committedInstructionFiles(root) {
55
57
  try {
56
58
  const raw = execFileSync("git", ["-C", root, "ls-tree", "-r", "-z", "--name-only", "HEAD"], {
57
59
  encoding: "buffer",
60
+ env: replacementFreeGitEnvironment(),
58
61
  maxBuffer: 16 * 1024 * 1024,
59
62
  stdio: ["ignore", "pipe", "ignore"],
60
63
  });
@@ -69,12 +72,14 @@ function committedFileAt(root, revision, file, maxBytes) {
69
72
  const object = `${revision}:${file}`;
70
73
  const size = Number(execFileSync("git", ["-C", root, "cat-file", "-s", object], {
71
74
  encoding: "utf8",
75
+ env: replacementFreeGitEnvironment(),
72
76
  stdio: ["ignore", "pipe", "ignore"],
73
77
  }).trim());
74
78
  if (!Number.isFinite(size) || size < 0 || size > maxBytes)
75
79
  return null;
76
80
  return execFileSync("git", ["-C", root, "cat-file", "blob", object], {
77
81
  encoding: "utf8",
82
+ env: replacementFreeGitEnvironment(),
78
83
  maxBuffer: maxBytes + 1,
79
84
  stdio: ["ignore", "pipe", "ignore"],
80
85
  });
@@ -102,6 +107,7 @@ function authoredInstructionCommit(root, file, sourceHash) {
102
107
  file,
103
108
  ], {
104
109
  encoding: "utf8",
110
+ env: replacementFreeGitEnvironment(),
105
111
  maxBuffer: 1024 * 1024,
106
112
  stdio: ["ignore", "pipe", "ignore"],
107
113
  }).split(/\r?\n/).filter(Boolean);
@@ -135,12 +141,12 @@ function instructionEvents(root, dataClass, policies, constraints) {
135
141
  totalBytes += Buffer.byteLength(source, "utf8");
136
142
  const sourceHash = canonicalHash(source);
137
143
  const introducedAt = authoredInstructionCommit(root, file, sourceHash);
138
- if (!introducedAt || !revExists(introducedAt, root)) {
144
+ const commit = introducedAt ? replacementFreeExactCommit(root, introducedAt) : null;
145
+ if (!commit) {
139
146
  excluded++;
140
147
  continue;
141
148
  }
142
- const commit = revParse(`${introducedAt}^{commit}`, root);
143
- const meta = commitMeta(commit, root);
149
+ const meta = replacementFreeCommitMeta(root, commit);
144
150
  if (!meta || !Number.isFinite(Date.parse(meta.date))) {
145
151
  excluded++;
146
152
  continue;
@@ -213,9 +219,10 @@ function importEvents(root, file, store, repository, opts, policies, constraints
213
219
  }
214
220
  let commit;
215
221
  if (item.commit) {
216
- if (!revExists(item.commit, root))
222
+ const exactCommit = replacementFreeExactCommit(root, item.commit);
223
+ if (!exactCommit)
217
224
  throw new Error(`evidence import item ${item.id} commit ${item.commit} does not resolve`);
218
- commit = revParse(`${item.commit}^{commit}`, root);
225
+ commit = exactCommit;
219
226
  }
220
227
  const files = [...new Set(item.files.map(normalizeRepoFile))].sort();
221
228
  const symbols = [...new Set(item.symbols)].sort();
@@ -294,7 +301,9 @@ function isPrivateRecord(store, kind, id, opts) {
294
301
  return false;
295
302
  return !!store.getPrivateRec(kind, id);
296
303
  }
297
- function correctionEvent(root, constraint, isPrivate) {
304
+ /** Canonical evidence projection for one captured correction. Materializers may
305
+ * reuse this projection, but ingestion itself remains evidence-only. */
306
+ export function correctionEvidenceEvent(root, constraint, dataClass) {
298
307
  const occurredAt = constraint.valid_from ?? constraint.provenance.last_verified;
299
308
  if (!occurredAt || !Number.isFinite(Date.parse(occurredAt)))
300
309
  return null;
@@ -306,16 +315,16 @@ function correctionEvent(root, constraint, isPrivate) {
306
315
  match: constraint.match,
307
316
  source_decision: constraint.source_decision,
308
317
  });
309
- const event = EvidenceEventSchema.parse({
318
+ return EvidenceEventSchema.parse({
310
319
  id: `ev_${shortHash(`correction:${contentHash}`)}`,
311
320
  kind: "correction",
312
321
  occurred_at: occurredAt,
313
- repository: basename(root),
322
+ repository: stableRepositoryName(root),
314
323
  files: constraint.scope.filter((scope) => scope !== "**"),
315
324
  symbols: constraint.forbids?.symbols ?? [],
316
325
  text_ref: constraint.id,
317
326
  related_records: [constraint.id, ...(constraint.source_decision ? [constraint.source_decision] : [])],
318
- data_class: isPrivate ? "private" : "public",
327
+ data_class: dataClass,
319
328
  content_hash: contentHash,
320
329
  compiler: {
321
330
  status: "covered",
@@ -329,11 +338,19 @@ function correctionEvent(root, constraint, isPrivate) {
329
338
  last_verified: constraint.provenance.last_verified,
330
339
  },
331
340
  });
332
- return { occurredAt, event, private: isPrivate };
341
+ }
342
+ function correctionEvent(store, root, constraint, isPrivate) {
343
+ // A public evidence event may only name a source decision that is itself
344
+ // public. Missing/private-only ids are excluded before serialization so a
345
+ // machine without the overlay cannot accidentally declassify the reference.
346
+ if (!isPrivate && constraint.source_decision && !store.json.get("decisions", constraint.source_decision))
347
+ return null;
348
+ const event = correctionEvidenceEvent(root, constraint, isPrivate ? "private" : "public");
349
+ return event ? { occurredAt: event.occurred_at, event, private: isPrivate } : null;
333
350
  }
334
351
  function bugEvent(root, store, repository, bug, isPrivate, opts) {
335
352
  const commit = bug.lineage.fixed_commit ?? bug.lineage.introduced_commit;
336
- const meta = commit ? commitMeta(commit, root) : null;
353
+ const meta = commit ? replacementFreeCommitMeta(root, commit) : null;
337
354
  const occurredAt = meta?.date ?? bug.provenance.last_verified;
338
355
  if (!occurredAt || !Number.isFinite(Date.parse(occurredAt)))
339
356
  return null;
@@ -411,11 +428,11 @@ export function ingestLocalEvidence(store, root, repository, opts = {}) {
411
428
  let scanned = records.constraints.length + records.bugs.length;
412
429
  let excluded = 0;
413
430
  for (const constraint of records.constraints) {
414
- if (constraint.status !== "active" || !constraint.provenance.source.includes("human_confirmed")) {
431
+ if (constraint.status !== "active" || !isHumanConfirmed(constraint.provenance.source)) {
415
432
  excluded++;
416
433
  continue;
417
434
  }
418
- const item = correctionEvent(root, constraint, isPrivateRecord(store, "constraints", constraint.id, opts));
435
+ const item = correctionEvent(store, root, constraint, isPrivateRecord(store, "constraints", constraint.id, opts));
419
436
  if (!item || Date.parse(item.occurredAt) < minDate)
420
437
  excluded++;
421
438
  else
@@ -1,13 +1,15 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
2
  import { dirname, join } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
- import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
- import { headSha, revExists } from "../extractors/git.js";
6
+ import { headSha } from "../extractors/git.js";
7
7
  import { canonicalHash } from "./canonical.js";
8
8
  import { nodeTestInfrastructureError } from "./g2BehaviorCandidates.js";
9
- import { dependencySnapshotForCommit } from "./g2BehaviorDependencies.js";
9
+ import { dependencySnapshotForCommit, materializeDependencySnapshot } from "./g2BehaviorDependencies.js";
10
10
  import { cleanupReplayWorktree, hasUnsafeReplayFilter, replayGitArgs } from "./replay.js";
11
+ import { hasUnsafeCheckoutAttributes } from "./safeCheckout.js";
12
+ import { replacementFreeExactCommit, replacementFreeGitEnvironment } from "./replacementFreeGit.js";
11
13
  import { BEHAVIOR_POLICY_EVALUATOR, PolicyEvaluationSchema, } from "./schema.js";
12
14
  import { NODE_TEST_REPORTER_SOURCE, exactNodeTestPattern, nodeTestIsolationFlag, nodeTestReporterEvents, } from "./nodeTestEvidence.js";
13
15
  import { applyBehaviorWorkspace, BehaviorWorkspaceError, captureBehaviorWorkspace, } from "./behaviorWorkspace.js";
@@ -22,6 +24,7 @@ function behaviorEnvironment(home, gitConfig) {
22
24
  HOME: home,
23
25
  GIT_CONFIG_GLOBAL: gitConfig,
24
26
  GIT_CONFIG_NOSYSTEM: "1",
27
+ GIT_NO_REPLACE_OBJECTS: "1",
25
28
  GIT_TERMINAL_PROMPT: "0",
26
29
  GIT_LFS_SKIP_SMUDGE: "1",
27
30
  HUNCH_PRIVATE_DIR: "",
@@ -31,10 +34,17 @@ function behaviorEnvironment(home, gitConfig) {
31
34
  FORCE_COLOR: "0",
32
35
  };
33
36
  }
37
+ function behaviorWorkspaceCaptureEnvironment(home, gitConfig) {
38
+ const env = behaviorEnvironment(home, gitConfig);
39
+ if (process.env.GIT_INDEX_FILE)
40
+ env.GIT_INDEX_FILE = process.env.GIT_INDEX_FILE;
41
+ return env;
42
+ }
34
43
  function sourceAt(root, commit, file) {
35
44
  try {
36
45
  return execFileSync("git", ["-C", root, "show", `${commit}:${file}`], {
37
46
  encoding: "utf8",
47
+ env: replacementFreeGitEnvironment(),
38
48
  maxBuffer: 10 * 1024 * 1024,
39
49
  stdio: ["ignore", "pipe", "ignore"],
40
50
  });
@@ -74,7 +84,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
74
84
  let workspace;
75
85
  if (opts.workspace && /^[a-f0-9]{40}$/.test(commit)) {
76
86
  const probe = mkdtempSync(join(tmpdir(), "hunch-behavior-workspace-probe-"));
77
- const env = behaviorEnvironment(probe, join(probe, "global.gitconfig"));
87
+ const env = behaviorWorkspaceCaptureEnvironment(probe, join(probe, "global.gitconfig"));
78
88
  try {
79
89
  writeFileSync(join(probe, "global.gitconfig"), "");
80
90
  workspace = captureBehaviorWorkspace(root, commit, opts.workspace, env);
@@ -103,7 +113,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
103
113
  exit_code: null,
104
114
  selected_event: null,
105
115
  };
106
- if (!/^[a-f0-9]{40}$/.test(commit) || !revExists(commit, root)) {
116
+ if (!/^[a-f0-9]{40}$/.test(commit) || replacementFreeExactCommit(root, commit) !== commit) {
107
117
  return evaluation(policy, baseExecution.commit, { ...baseExecution, error_code: "commit-ref-unresolved" }, "error", "executable behavior commit does not resolve");
108
118
  }
109
119
  const source = sourceAt(root, assertion.test.source_commit, assertion.test.file);
@@ -132,6 +142,9 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
132
142
  if (hasUnsafeReplayFilter(root, env)) {
133
143
  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
144
  }
145
+ else if (hasUnsafeCheckoutAttributes(root, commit, env, { allowDisabledLfs: true })) {
146
+ result = evaluation(policy, commit, { ...baseExecution, commit, dependency_snapshot_id: dependency.snapshot.id, error_code: "unsafe-checkout-attributes" }, "error", "repository has checkout attributes that could transform or merge exact source bytes");
147
+ }
135
148
  else {
136
149
  execFileSync("git", replayGitArgs(root, hooks, ["worktree", "add", "--detach", "--force", checkout, commit]), {
137
150
  env,
@@ -141,7 +154,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
141
154
  added = true;
142
155
  if (workspace)
143
156
  applyBehaviorWorkspace(root, checkout, workspace, env);
144
- symlinkSync(dependency.nodeModules, join(checkout, "node_modules"), process.platform === "win32" ? "junction" : "dir");
157
+ materializeDependencySnapshot(dependency, join(checkout, "node_modules"));
145
158
  const testFile = join(checkout, assertion.test.file);
146
159
  mkdirSync(dirname(testFile), { recursive: true });
147
160
  writeFileSync(testFile, source);
@@ -155,7 +168,7 @@ export function evaluateExecutableBehaviorPolicy(root, policy, opts = {}) {
155
168
  ];
156
169
  const args = assertion.runner === "node-test"
157
170
  ? common
158
- : [join(dependency.nodeModules, "tsx", "dist", "cli.mjs"), ...common];
171
+ : [join(checkout, "node_modules", "tsx", "dist", "cli.mjs"), ...common];
159
172
  const run = spawnSync(process.execPath, args, {
160
173
  cwd: checkout,
161
174
  env,
@@ -1,4 +1,3 @@
1
- import { basename } from "node:path";
2
1
  import { shortHash } from "../core/ids.js";
3
2
  import { headSha } from "../extractors/git.js";
4
3
  import { evaluateExecutableBehaviorPolicy } from "./behaviorEvaluator.js";
@@ -81,7 +80,9 @@ export function createExecutableBehaviorProofPlan(root, repository, policy, opts
81
80
  const body = {
82
81
  policy_id: policy.id,
83
82
  policy_candidate_hash: policyHash,
84
- repository: basename(root),
83
+ // Bind to the immutable corpus label so a teammate in a differently named
84
+ // clone reuses the same executable-behavior plan instead of minting a fork.
85
+ repository: corpus.repository,
85
86
  data_class: policy.data_class,
86
87
  source_commit: policy.assertion.test.source_commit,
87
88
  valid_from_commit: policy.assertion.test.source_commit,
@@ -1,11 +1,12 @@
1
1
  import { sha1, shortHash } from "../core/ids.js";
2
+ import { compareCodeUnits } from "../core/canonicalOrder.js";
2
3
  function canonicalValue(value) {
3
4
  if (Array.isArray(value))
4
5
  return value.map(canonicalValue);
5
6
  if (value && typeof value === "object") {
6
7
  return Object.fromEntries(Object.entries(value)
7
8
  .filter(([, v]) => v !== undefined)
8
- .sort(([a], [b]) => a.localeCompare(b))
9
+ .sort(([a], [b]) => compareCodeUnits(a, b))
9
10
  .map(([k, v]) => [k, canonicalValue(v)]));
10
11
  }
11
12
  return value;
@@ -23,6 +24,11 @@ export function canonicalHash(value) {
23
24
  export function policySemanticHash(policy) {
24
25
  return canonicalHash({
25
26
  id: policy.id,
27
+ // Preserve existing generic-policy hashes while binding the correction
28
+ // compiler's provenance and permanent MD-1a activation gate into every
29
+ // correction proof. Removing either makes that proof stale.
30
+ ...(policy.origin !== "generic" ? { origin: policy.origin } : {}),
31
+ ...(policy.activation_gate ? { activation_gate: policy.activation_gate } : {}),
26
32
  ir_version: policy.ir_version,
27
33
  statement: policy.statement,
28
34
  scope: policy.scope,
@@ -14,8 +14,9 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
14
14
  const baselineClean = proof.current.total === 1 && proof.current.satisfied === 1 && proof.current.unknown === 0 && proof.current.error === 0;
15
15
  const dispositionAssessment = assessHistoryDispositions(proof, dispositions);
16
16
  const evidenceError = blockingEvidenceError(proof, dispositions);
17
+ const activationError = policy.activation_gate?.status === "blocked" ? policy.activation_gate.reason : null;
17
18
  const proofStrongEnough = proofRank[proof.proof_class] >= proofRank.P3;
18
- const eligible = semanticMatch && baselineClean && proofStrongEnough && !evidenceError;
19
+ const eligible = semanticMatch && baselineClean && proofStrongEnough && !evidenceError && !activationError;
19
20
  const canBlock = eligible && policy.state === "active_blocking" && policy.severity === "blocking" && policy.authority?.kind === "human";
20
21
  const unknownResults = proof.current.unknown + proof.known_bad.unknown + proof.known_good.unknown + proof.accepted_history.unknown + proof.mutations.unknown;
21
22
  const errorResults = proof.current.error + proof.known_bad.error + proof.known_good.error + proof.accepted_history.error + proof.mutations.error;
@@ -36,6 +37,8 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
36
37
  actions.push("Repair every failed required mutation control before considering blocking approval.");
37
38
  if (policy.candidate.conflicts.length)
38
39
  actions.push("Resolve every direct candidate conflict with a human disposition before lifecycle promotion.");
40
+ if (activationError)
41
+ actions.push(`Activation is mechanically blocked: ${activationError}`);
39
42
  if (shadowPrecision?.recommendation === "eligible_for_p4_review")
40
43
  actions.push("Shadow thresholds are met; a human may review P4 evidence, but measurement grants no authority.");
41
44
  else if (shadowPrecision)
@@ -57,6 +60,7 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
57
60
  evidence: [...policy.evidence],
58
61
  candidate: policy.candidate,
59
62
  exception_of: policy.exception_of,
63
+ activation_gate: policy.activation_gate,
60
64
  },
61
65
  proof: { id: proof.id, proof_class: proof.proof_class, plan_hash: proof.plan_hash, generated_at: proof.generated_at },
62
66
  evidence_vector: {
@@ -88,7 +92,7 @@ export function buildProofCard(policy, proof, dispositions = [], composition = [
88
92
  current: policy.authority,
89
93
  eligible_for_human_blocking_approval: eligible,
90
94
  can_block_now: canBlock,
91
- blocking_evidence_error: evidenceError,
95
+ blocking_evidence_error: activationError ?? evidenceError,
92
96
  },
93
97
  actions,
94
98
  };
@@ -108,6 +112,7 @@ export function renderProofCard(card) {
108
112
  ` candidate alternatives: ${card.policy.candidate.alternatives.length} · conflicts: ${card.policy.candidate.conflicts.length} · incumbent: ${card.policy.candidate.incumbent ?? "none"}`,
109
113
  ` scope suggestion: ${card.policy.candidate.scope_suggestion ? JSON.stringify(card.policy.candidate.scope_suggestion) : "none — narrow compiled scope retained"}`,
110
114
  ` exception parent: ${card.policy.exception_of ?? "none"}`,
115
+ ...(card.policy.activation_gate ? [` activation gate: ${card.policy.activation_gate.status} — ${card.policy.activation_gate.reason}`] : []),
111
116
  ` ${line("current", card.evidence_vector.current)}`,
112
117
  ` ${line("known bad", card.evidence_vector.known_bad)}`,
113
118
  ` ${line("known good", card.evidence_vector.known_good)}`,
@@ -1,5 +1,5 @@
1
1
  import { policyId } from "./canonical.js";
2
- import { POLICY_IR_VERSION, PolicySpecSchema } from "./schema.js";
2
+ import { CORRECTION_POLICY_IR_VERSION, POLICY_IR_VERSION, PolicySpecSchema } from "./schema.js";
3
3
  function selector(ref) {
4
4
  if (ref.startsWith("symbol-id:") || ref.startsWith("symbol:"))
5
5
  return { selector: ref };
@@ -173,4 +173,74 @@ export function compileStructuralPolicy(store, input) {
173
173
  });
174
174
  return { policy, private: isPrivate };
175
175
  }
176
+ /** Compile one already-bound correction projection. The caller must first prove
177
+ * that the projection has exactly one supported structural interpretation. */
178
+ export function compileCorrectionPolicy(store, input) {
179
+ const isPrivate = input.dataClass !== "public";
180
+ if (isPrivate && !store.hasPrivate)
181
+ throw new Error("private correction compilation needs a configured Hunch private overlay");
182
+ const now = input.now ?? new Date().toISOString();
183
+ const id = policyId({ assertion: input.assertion, scope: input.scope, data_class: input.dataClass });
184
+ const refs = [...new Set([
185
+ input.source.id,
186
+ input.evidenceId,
187
+ ...(input.source.source_decision ? [input.source.source_decision] : []),
188
+ ])].sort();
189
+ const policy = PolicySpecSchema.parse({
190
+ id,
191
+ topic: `correction.${input.source.id}`,
192
+ origin: "correction_md1a",
193
+ ir_version: CORRECTION_POLICY_IR_VERSION,
194
+ revision: 1,
195
+ state: "compiled",
196
+ statement: input.source.statement,
197
+ rationale: input.source.rationale,
198
+ scope: input.scope,
199
+ assertion: input.assertion,
200
+ severity: "warning",
201
+ surfaces: ["cli", "mcp", "ci"],
202
+ authority: null,
203
+ activation_gate: {
204
+ kind: "source_currentness",
205
+ status: "blocked",
206
+ reason: "MD-1a correction projections cannot activate until MD-2 proves that every source correction and decision is still current.",
207
+ },
208
+ evidence: refs,
209
+ proof: null,
210
+ reversal_conditions: [
211
+ `Source correction ${input.source.id} is retired or its exact structural interpretation is rejected.`,
212
+ ...(input.source.source_decision ? [`Source decision ${input.source.source_decision} is superseded or explicitly closed.`] : []),
213
+ ],
214
+ supersedes: null,
215
+ superseded_by: null,
216
+ exception_of: null,
217
+ valid_from: null,
218
+ valid_to: null,
219
+ data_class: input.dataClass,
220
+ limitations: [
221
+ "Scope is intentionally limited to one exact file and anchored to one stable symbol in that file.",
222
+ "TypeScript/JavaScript static ESM import declarations only; re-exports, require(), dynamic import(), package aliases, and runtime loading are not covered.",
223
+ "The immediate legacy Constraint remains the fast guard; this MD-1a policy cannot activate until the source-currentness gate is implemented and cleared.",
224
+ ],
225
+ candidate: input.candidate,
226
+ legacy_refs: [input.source.id, ...(input.source.source_decision ? [input.source.source_decision] : [])],
227
+ audit: [{
228
+ action: "compiled",
229
+ actor_kind: "system",
230
+ actor: "hunch:correction-policy-materializer",
231
+ at: now,
232
+ reason: `Compiled from one deterministic supported projection bound to correction ${input.source.id}.`,
233
+ proof: null,
234
+ }],
235
+ created_at: now,
236
+ updated_at: now,
237
+ provenance: {
238
+ source: "derived",
239
+ confidence: 1,
240
+ evidence: refs,
241
+ last_verified: now,
242
+ },
243
+ });
244
+ return { policy, private: isPrivate };
245
+ }
176
246
  //# sourceMappingURL=compiler.js.map