@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,241 @@
1
+ import { basename } from "node:path";
2
+ import { z } from "zod";
3
+ import { shortHash } from "../core/ids.js";
4
+ import { headSha } from "../extractors/git.js";
5
+ import { canonicalHash, policyId, policySemanticHash } from "./canonical.js";
6
+ import { compileProofCorpus } from "./corpus.js";
7
+ import { assessG2BehaviorMaterialization } from "./g2BehaviorMaterialization.js";
8
+ import { dependencySnapshotById, dependencySnapshotForCommit, provisionG2BehaviorDependencySnapshotsForCommits, } from "./g2BehaviorDependencies.js";
9
+ import { g2BehaviorCandidateHash, } from "./g2BehaviorCandidates.js";
10
+ import { proposeProvedPolicy } from "./lifecycle.js";
11
+ import { createProofPlan } from "./plan.js";
12
+ import { provePolicy } from "./proof.js";
13
+ import { EXECUTABLE_BEHAVIOR_IR_VERSION, PolicySpecSchema, } from "./schema.js";
14
+ const HASH = /^sha1:[a-f0-9]{40}$/;
15
+ const MaterializedItemSchema = z.object({
16
+ candidate_id: z.string().regex(/^g2behavior_[a-f0-9]{10}$/),
17
+ attestation_id: z.string().regex(/^g2behaviorattest_[a-f0-9]{10}$/),
18
+ policy_id: z.string().regex(/^pol_[a-f0-9]{10}$/),
19
+ policy_hash: z.string().regex(HASH),
20
+ corpus_id: z.string().regex(/^corpus_[a-f0-9]{10}$/),
21
+ corpus_hash: z.string().regex(HASH),
22
+ plan_id: z.string().regex(/^plan_[a-f0-9]{10}$/),
23
+ plan_hash: z.string().regex(HASH),
24
+ proof_id: z.string().regex(/^proof_[a-f0-9]{10}$/),
25
+ proof_hash: z.string().regex(HASH),
26
+ proof_class: z.literal("P3"),
27
+ policy_state: z.enum(["proposed", "active_advisory", "active_blocking"]),
28
+ }).strict();
29
+ export const G2BehaviorPolicyMaterializationSchema = z.object({
30
+ id: z.string().regex(/^g2behaviorpolicies_[a-f0-9]{10}$/),
31
+ content_hash: z.string().regex(HASH),
32
+ assessment_id: z.string().regex(/^g2behaviormaterialization_[a-f0-9]{10}$/),
33
+ assessment_hash: z.string().regex(HASH),
34
+ source_review_id: z.string().regex(/^g2behaviorcandidates_[a-f0-9]{10}$/),
35
+ source_review_hash: z.string().regex(HASH),
36
+ current_commit: z.string().regex(/^[a-f0-9]{40}$/),
37
+ dependency_snapshots: z.array(z.object({ id: z.string().regex(/^g2deps_[a-f0-9]{10}$/), content_hash: z.string().regex(HASH) }).strict()).min(1),
38
+ materialized_policies: z.number().int().min(1),
39
+ items: z.array(MaterializedItemSchema).min(1),
40
+ data_class: z.literal("private"),
41
+ authority: z.literal("none"),
42
+ effects: z.literal("policy_proposal_only"),
43
+ writes: z.literal("private_policy_artifacts"),
44
+ activation: z.literal("separate_human_action_required"),
45
+ }).strict();
46
+ function compileBehaviorPolicy(root, candidate, attestation, dependencySnapshotIds, now) {
47
+ const assertion = {
48
+ kind: "executable-behavior",
49
+ test: {
50
+ file: candidate.test.file,
51
+ name: candidate.test.name,
52
+ source_commit: candidate.proposed_corpus.known_good.ref,
53
+ source_hash: candidate.test.source_hash,
54
+ },
55
+ runner: candidate.runner.kind,
56
+ attestation: {
57
+ id: attestation.id,
58
+ content_hash: attestation.content_hash,
59
+ candidate_id: attestation.candidate_id,
60
+ candidate_hash: attestation.candidate_hash,
61
+ replay_id: attestation.replay_id,
62
+ replay_hash: attestation.replay_hash,
63
+ },
64
+ dependency_snapshot_ids: [...new Set(dependencySnapshotIds)].sort(),
65
+ timeout_ms: 30_000,
66
+ };
67
+ const id = policyId({ assertion, repository: basename(root), data_class: "private" });
68
+ const evidence = [...new Set([
69
+ attestation.id,
70
+ attestation.replay_id,
71
+ candidate.id,
72
+ ...candidate.decision_ids,
73
+ ...candidate.source_attestation_ids,
74
+ ])].sort();
75
+ return PolicySpecSchema.parse({
76
+ id,
77
+ topic: `g2.behavior.${candidate.id}`,
78
+ ir_version: EXECUTABLE_BEHAVIOR_IR_VERSION,
79
+ revision: 1,
80
+ state: "compiled",
81
+ statement: attestation.reason,
82
+ rationale: `Human-selected executable regression ${candidate.test.file} :: ${candidate.test.name}`,
83
+ scope: { repos: [basename(root)], paths: [], components: [] },
84
+ assertion,
85
+ severity: "warning",
86
+ surfaces: ["pre_commit", "ci", "mcp", "cli"],
87
+ authority: null,
88
+ evidence,
89
+ proof: null,
90
+ reversal_conditions: [`Behavior attestation ${attestation.id} is superseded, rejected, or its exact test/replay binding no longer verifies.`],
91
+ supersedes: null,
92
+ superseded_by: null,
93
+ exception_of: null,
94
+ valid_from: null,
95
+ valid_to: null,
96
+ data_class: "private",
97
+ limitations: [
98
+ "The assertion executes one exact hash-pinned node:test case from a human-selected fixing commit; it does not encode a helper symbol or call-edge proxy.",
99
+ "Proof and history replay use committed Git states; advisory delivery may evaluate a content-addressed staged or working snapshot in a disposable checkout.",
100
+ "Executable evidence creates no authority. Advisory or blocking activation requires a separate explicit human lifecycle action.",
101
+ ],
102
+ candidate: {
103
+ alternatives: [],
104
+ uncertainty: [],
105
+ conflicts: [],
106
+ incumbent: null,
107
+ scope_suggestion: null,
108
+ counterexamples: [],
109
+ },
110
+ legacy_refs: candidate.decision_ids,
111
+ audit: [{
112
+ action: "compiled",
113
+ actor_kind: "system",
114
+ actor: "hunch:behavior-materializer",
115
+ at: now,
116
+ reason: `Compiled only from current selected behavior attestation ${attestation.id}.`,
117
+ proof: null,
118
+ }],
119
+ created_at: now,
120
+ updated_at: now,
121
+ provenance: {
122
+ source: "human_confirmed+executable_regression",
123
+ confidence: 1,
124
+ evidence,
125
+ last_verified: now,
126
+ },
127
+ });
128
+ }
129
+ function exactSnapshotProjection(snapshots) {
130
+ return snapshots.map((snapshot) => ({ id: snapshot.id, content_hash: snapshot.content_hash }))
131
+ .sort((left, right) => left.id.localeCompare(right.id));
132
+ }
133
+ export function materializeSelectedG2BehaviorPolicies(store, root, repository, report, currentAttestations, opts = {}) {
134
+ if (!store.hasPrivate)
135
+ throw new Error("selected executable behavior materialization requires a configured private Hunch overlay");
136
+ const assessment = assessG2BehaviorMaterialization(report, currentAttestations);
137
+ if (assessment.readiness !== "ready_for_materialization")
138
+ throw new Error(`behavior assessment ${assessment.id} is not ready for materialization`);
139
+ const currentCommit = headSha(root);
140
+ if (!/^[a-f0-9]{40}$/.test(currentCommit))
141
+ throw new Error("behavior materialization requires a current full-SHA HEAD");
142
+ const candidateHashes = new Map(report.items.map((candidate) => [candidate.id, g2BehaviorCandidateHash(candidate)]));
143
+ const selected = currentAttestations.filter((attestation) => attestation.disposition === "selected"
144
+ && candidateHashes.get(attestation.candidate_id) === attestation.candidate_hash)
145
+ .sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
146
+ const dependencies = provisionG2BehaviorDependencySnapshotsForCommits(root, [currentCommit], opts.allowInstallScripts ?? [], opts.dependencyTimeoutMs ?? 300_000);
147
+ const currentDependencyId = dependencies.commits[0].dependency_snapshot_id;
148
+ const attestedSnapshots = selected.flatMap((attestation) => attestation.dependency_snapshot_ids.map((id) => {
149
+ const snapshot = dependencySnapshotById(root, id);
150
+ if (!snapshot)
151
+ throw new Error(`selected behavior attestation ${attestation.id} binds unavailable dependency snapshot ${id}`);
152
+ return snapshot.snapshot;
153
+ }));
154
+ for (const attestation of selected) {
155
+ const candidate = report.items.find((item) => item.id === attestation.candidate_id);
156
+ for (const commit of [candidate.proposed_corpus.known_bad.ref, candidate.proposed_corpus.known_good.ref]) {
157
+ if (!dependencySnapshotForCommit(root, commit, attestation.dependency_snapshot_ids)) {
158
+ throw new Error(`selected behavior attestation ${attestation.id} has no exact bound dependency snapshot for ${commit}`);
159
+ }
160
+ }
161
+ }
162
+ const allSnapshots = [...new Map([...dependencies.snapshots, ...attestedSnapshots].map((snapshot) => [snapshot.id, snapshot])).values()];
163
+ const now = opts.now ?? new Date().toISOString();
164
+ const items = selected.map((attestation) => {
165
+ const candidate = report.items.find((item) => item.id === attestation.candidate_id);
166
+ const compiled = compileBehaviorPolicy(root, candidate, attestation, [currentDependencyId, ...attestation.dependency_snapshot_ids], now);
167
+ const existingPolicy = repository.getPolicy(compiled.id, { privateOnly: true });
168
+ if (existingPolicy && policySemanticHash(existingPolicy) !== policySemanticHash(compiled)) {
169
+ throw new Error(`existing behavior policy ${compiled.id} has different semantics`);
170
+ }
171
+ let policy = existingPolicy ?? repository.putPolicy(compiled, { private: true });
172
+ const compiledCorpus = compileProofCorpus(root, policy, {
173
+ known_bad: [{ ref: candidate.proposed_corpus.known_bad.ref, label: `known-bad behavior before ${candidate.commit}` }],
174
+ known_good: [{
175
+ ref: candidate.proposed_corpus.known_good.ref,
176
+ label: `human-selected fixing behavior ${candidate.commit}`,
177
+ attestation: { actor: attestation.actor, reason: attestation.reason },
178
+ }],
179
+ }, { now });
180
+ const existingCorpus = repository.getCorpus(policy.id, { privateOnly: true });
181
+ if (existingCorpus && existingCorpus.content_hash !== compiledCorpus.content_hash) {
182
+ throw new Error(`existing behavior corpus ${existingCorpus.id} differs from exact selected evidence`);
183
+ }
184
+ const corpus = existingCorpus ?? repository.putCorpus(compiledCorpus, policy.id);
185
+ const generatedPlan = createProofPlan(store, root, repository, policy, { privateOnly: true, maxCommits: 0, maxMutations: 2, now });
186
+ const plan = repository.getPlan(generatedPlan.id, { privateOnly: true }) ?? repository.putPlan(generatedPlan, policy.id, { private: true });
187
+ const generatedProof = provePolicy(store, root, policy, { plan, now });
188
+ if (generatedProof.proof_class !== "P3") {
189
+ const outcomes = generatedProof.replay_receipts
190
+ .map((receipt) => `${receipt.leg}:${receipt.result}${receipt.error_code ? `(${receipt.error_code})` : ""}`)
191
+ .join(", ");
192
+ throw new Error(`behavior policy ${policy.id} proof is ${generatedProof.proof_class}; exact P3 is required for materialization (${outcomes})`);
193
+ }
194
+ const proof = repository.getProof(generatedProof.id, { privateOnly: true }) ?? repository.putProof(generatedProof, policy.id);
195
+ if (policy.state === "compiled" || policy.state === "validating" || policy.state === "proposed") {
196
+ const proposed = policy.state === "proposed" && policy.proof === proof.id
197
+ ? policy
198
+ : proposeProvedPolicy(policy, proof, now, [], currentAttestations);
199
+ policy = proposed === policy ? policy : repository.putPolicy(proposed, { private: true });
200
+ }
201
+ else if (policy.proof !== proof.id) {
202
+ throw new Error(`behavior policy ${policy.id} is ${policy.state} with a different proof`);
203
+ }
204
+ return {
205
+ candidate_id: candidate.id,
206
+ attestation_id: attestation.id,
207
+ policy_id: policy.id,
208
+ policy_hash: policySemanticHash(policy),
209
+ corpus_id: corpus.id,
210
+ corpus_hash: corpus.content_hash,
211
+ plan_id: plan.id,
212
+ plan_hash: plan.content_hash,
213
+ proof_id: proof.id,
214
+ proof_hash: canonicalHash(proof),
215
+ proof_class: proof.proof_class,
216
+ policy_state: policy.state,
217
+ };
218
+ });
219
+ const body = {
220
+ assessment_id: assessment.id,
221
+ assessment_hash: assessment.content_hash,
222
+ source_review_id: report.id,
223
+ source_review_hash: report.content_hash,
224
+ current_commit: currentCommit,
225
+ dependency_snapshots: exactSnapshotProjection(allSnapshots),
226
+ materialized_policies: items.length,
227
+ items,
228
+ data_class: "private",
229
+ authority: "none",
230
+ effects: "policy_proposal_only",
231
+ writes: "private_policy_artifacts",
232
+ activation: "separate_human_action_required",
233
+ };
234
+ const contentHash = canonicalHash(body);
235
+ return G2BehaviorPolicyMaterializationSchema.parse({
236
+ id: `g2behaviorpolicies_${shortHash(contentHash)}`,
237
+ content_hash: contentHash,
238
+ ...body,
239
+ });
240
+ }
241
+ //# sourceMappingURL=g2BehaviorPolicyMaterializer.js.map
@@ -0,0 +1,179 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { writeFileAtomic } from "../core/io.js";
5
+ import { shortHash } from "../core/ids.js";
6
+ import { canonicalHash } from "./canonical.js";
7
+ import { g2CandidateItemHash, g2CandidateReviewContentHash, } from "./g2Candidates.js";
8
+ const HASH = /^sha1:[a-f0-9]{40}$/;
9
+ const HUMAN_ACTOR = /^(human|github|git):[^\s]+$/i;
10
+ const encode = (value) => `${JSON.stringify(value, null, 2)}\n`;
11
+ export const G2CandidateDispositionSchema = z.enum(["selected", "rejected"]);
12
+ export const G2CandidateAttestationSchema = z.object({
13
+ id: z.string().regex(/^g2attest_[a-f0-9]{10}$/),
14
+ content_hash: z.string().regex(HASH),
15
+ candidate_id: z.string().regex(/^g2candidate_[a-f0-9]{10}$/),
16
+ candidate_hash: z.string().regex(HASH),
17
+ structural_candidate_id: z.string().regex(/^cand_[a-f0-9]{10}$/),
18
+ commit: z.string().regex(/^[a-f0-9]{40}$/),
19
+ review_hash: z.string().regex(HASH),
20
+ disposition: G2CandidateDispositionSchema,
21
+ actor: z.string().regex(HUMAN_ACTOR, "candidate attestation requires an explicit human actor (human:, github:, or git:)"),
22
+ reason: z.string().trim().min(1).max(4000),
23
+ supersedes: z.string().regex(/^g2attest_[a-f0-9]{10}$/).nullable(),
24
+ data_class: z.literal("private"),
25
+ authority: z.literal("none"),
26
+ effects: z.literal("review_only"),
27
+ created_at: z.string().datetime({ offset: true }),
28
+ }).strict();
29
+ export function g2CandidateAttestationContentHash(attestation) {
30
+ const { id: _id, content_hash: _contentHash, ...body } = attestation;
31
+ return canonicalHash(body);
32
+ }
33
+ export function compileG2CandidateAttestation(report, candidateId, reviewHash, disposition, actor, reason, opts = {}) {
34
+ if (report.content_hash !== g2CandidateReviewContentHash(report) || report.id !== `g2candidates_${shortHash(report.content_hash)}`) {
35
+ throw new Error(`G2 candidate review ${report.id} content hash mismatch`);
36
+ }
37
+ if (reviewHash !== report.content_hash)
38
+ throw new Error("candidate review hash does not match the exact current review packet");
39
+ const candidate = report.items.find((item) => item.id === candidateId);
40
+ if (!candidate)
41
+ throw new Error(`candidate ${candidateId} is not present in review ${report.id}`);
42
+ const body = {
43
+ candidate_id: candidate.id,
44
+ candidate_hash: g2CandidateItemHash(candidate),
45
+ structural_candidate_id: candidate.candidate_id,
46
+ commit: candidate.commit,
47
+ review_hash: report.content_hash,
48
+ disposition: G2CandidateDispositionSchema.parse(disposition),
49
+ actor,
50
+ reason: reason.trim(),
51
+ supersedes: opts.supersedes ?? null,
52
+ data_class: "private",
53
+ authority: "none",
54
+ effects: "review_only",
55
+ created_at: opts.now ?? new Date().toISOString(),
56
+ };
57
+ const contentHash = canonicalHash(body);
58
+ return G2CandidateAttestationSchema.parse({
59
+ id: `g2attest_${shortHash(contentHash)}`,
60
+ content_hash: contentHash,
61
+ ...body,
62
+ });
63
+ }
64
+ /** Resolve exact-candidate supersession chains without timestamp trust. */
65
+ export function currentG2CandidateAttestations(records) {
66
+ const parsed = records.map((record) => G2CandidateAttestationSchema.parse(record));
67
+ const byId = new Map(parsed.map((record) => [record.id, record]));
68
+ if (byId.size !== parsed.length)
69
+ throw new Error("duplicate G2 candidate attestation id");
70
+ const identity = (record) => `${record.candidate_id}:${record.candidate_hash}`;
71
+ const childCount = new Map();
72
+ for (const record of parsed) {
73
+ if (!record.supersedes)
74
+ continue;
75
+ const parent = byId.get(record.supersedes);
76
+ if (!parent)
77
+ throw new Error(`G2 candidate attestation ${record.id} supersedes missing ${record.supersedes}`);
78
+ if (identity(parent) !== identity(record)
79
+ || parent.structural_candidate_id !== record.structural_candidate_id
80
+ || parent.commit !== record.commit) {
81
+ throw new Error(`G2 candidate attestation ${record.id} supersedes a different exact candidate`);
82
+ }
83
+ childCount.set(parent.id, (childCount.get(parent.id) ?? 0) + 1);
84
+ if (childCount.get(parent.id) > 1)
85
+ throw new Error(`G2 candidate attestation ${parent.id} has a branched supersession chain`);
86
+ }
87
+ for (const record of parsed) {
88
+ const visited = new Set();
89
+ let cursor = record;
90
+ while (cursor?.supersedes) {
91
+ if (visited.has(cursor.id))
92
+ throw new Error(`G2 candidate attestation chain contains a cycle at ${cursor.id}`);
93
+ visited.add(cursor.id);
94
+ cursor = byId.get(cursor.supersedes);
95
+ }
96
+ }
97
+ const current = parsed.filter((record) => !childCount.has(record.id));
98
+ const currentTargets = new Set();
99
+ for (const record of current) {
100
+ const key = identity(record);
101
+ if (currentTargets.has(key))
102
+ throw new Error(`G2 candidate ${key} has multiple current attestations`);
103
+ currentTargets.add(key);
104
+ }
105
+ return current.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id) || left.id.localeCompare(right.id));
106
+ }
107
+ export class G2CandidateAttestationRepository {
108
+ store;
109
+ constructor(store) {
110
+ this.store = store;
111
+ }
112
+ list() {
113
+ const dir = this.store.privateDir ? join(this.store.privateDir, "candidate-attestations") : undefined;
114
+ if (!dir || !existsSync(dir))
115
+ return [];
116
+ const records = readdirSync(dir)
117
+ .filter((name) => name.endsWith(".json"))
118
+ .sort()
119
+ .map((name) => {
120
+ try {
121
+ if (!/^g2attest_[a-f0-9]{10}\.json$/.test(name))
122
+ throw new Error("unexpected attestation filename");
123
+ const parsed = G2CandidateAttestationSchema.parse(JSON.parse(readFileSync(join(dir, name), "utf8")));
124
+ if (parsed.content_hash !== g2CandidateAttestationContentHash(parsed)
125
+ || parsed.id !== `g2attest_${shortHash(parsed.content_hash)}`) {
126
+ throw new Error(`G2 candidate attestation ${parsed.id} content hash mismatch`);
127
+ }
128
+ if (name !== `${parsed.id}.json`)
129
+ throw new Error(`filename does not match attestation ${parsed.id}`);
130
+ return parsed;
131
+ }
132
+ catch (error) {
133
+ throw new Error(`invalid candidate-attestations/${name}: ${error.message}`);
134
+ }
135
+ });
136
+ currentG2CandidateAttestations(records);
137
+ return records;
138
+ }
139
+ current() {
140
+ return currentG2CandidateAttestations(this.list());
141
+ }
142
+ resolutions() {
143
+ return this.current().map((record) => ({
144
+ id: record.id,
145
+ candidate_id: record.candidate_id,
146
+ candidate_hash: record.candidate_hash,
147
+ review_hash: record.review_hash,
148
+ disposition: record.disposition,
149
+ actor: record.actor,
150
+ reason: record.reason,
151
+ created_at: record.created_at,
152
+ }));
153
+ }
154
+ put(attestation) {
155
+ if (!this.store.privateDir)
156
+ throw new Error("No private Hunch overlay is configured; refusing to write G2 candidate attestation.");
157
+ const parsed = G2CandidateAttestationSchema.parse(attestation);
158
+ if (parsed.content_hash !== g2CandidateAttestationContentHash(parsed)
159
+ || parsed.id !== `g2attest_${shortHash(parsed.content_hash)}`) {
160
+ throw new Error(`G2 candidate attestation ${parsed.id} content hash mismatch`);
161
+ }
162
+ const records = this.list();
163
+ const existing = records.find((record) => record.id === parsed.id);
164
+ if (existing)
165
+ return existing;
166
+ const current = currentG2CandidateAttestations(records).find((record) => (record.candidate_id === parsed.candidate_id && record.candidate_hash === parsed.candidate_hash));
167
+ if (current && parsed.supersedes !== current.id) {
168
+ throw new Error(`G2 candidate attestation ${current.id} is current; pass supersedes:${current.id} to append a correction`);
169
+ }
170
+ if (!current && parsed.supersedes)
171
+ throw new Error(`G2 candidate attestation ${parsed.id} supersedes no current exact candidate review`);
172
+ currentG2CandidateAttestations([...records, parsed]);
173
+ const dir = join(this.store.privateDir, "candidate-attestations");
174
+ mkdirSync(dir, { recursive: true });
175
+ writeFileAtomic(join(dir, `${parsed.id}.json`), encode(parsed));
176
+ return parsed;
177
+ }
178
+ }
179
+ //# sourceMappingURL=g2CandidateAttestation.js.map
@@ -0,0 +1,195 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { shortHash } from "../core/ids.js";
5
+ import { hunchPathsForDir } from "../core/paths.js";
6
+ import { commitMeta, fixCommits } from "../extractors/git.js";
7
+ import { indexRepo } from "../extractors/indexer.js";
8
+ import { HunchStore } from "../store/hunchStore.js";
9
+ import { canonicalHash } from "./canonical.js";
10
+ import { extractStructuralDelta } from "./delta.js";
11
+ import { enumerateStructuralCandidates } from "./structural.js";
12
+ export function g2CandidateItemHash(item) {
13
+ const { human_review: _humanReview, ...body } = item;
14
+ return canonicalHash(body);
15
+ }
16
+ export function g2CandidateReviewContentHash(report) {
17
+ const { id: _id, content_hash: _contentHash, ...body } = report;
18
+ return canonicalHash(body);
19
+ }
20
+ const attestationRank = {
21
+ human_grounded_exact: 0,
22
+ human_grounded_needs_selection: 1,
23
+ unattested_structural_coincidence: 2,
24
+ };
25
+ const basisRank = {
26
+ "added-call": 0,
27
+ "removed-call": 0,
28
+ "removed-import": 1,
29
+ "added-relative-import": 1,
30
+ "removed-relative-import": 1,
31
+ "added-symbol": 2,
32
+ };
33
+ export function positiveBound(value, label, max) {
34
+ if (!Number.isInteger(value) || value < 1)
35
+ throw new Error(`${label} must be a positive integer`);
36
+ if (value > max)
37
+ throw new Error(`${label} cannot exceed ${max}`);
38
+ return value;
39
+ }
40
+ function privateGrounding(decisionStore, graphStore, root) {
41
+ const byCommit = new Map();
42
+ if (!decisionStore.hasPrivate)
43
+ return byCommit;
44
+ const decisions = decisionStore.recsInHome("decisions", "private")
45
+ .filter((decision) => decision.status === "accepted"
46
+ && !decision.superseded_by
47
+ && !decision.valid_to
48
+ && !!decision.commit
49
+ && decision.provenance.source.includes("human_confirmed"));
50
+ for (const decision of decisions) {
51
+ const meta = commitMeta(decision.commit, root);
52
+ if (!meta)
53
+ continue;
54
+ try {
55
+ const delta = extractStructuralDelta(root, meta.sha);
56
+ const inspection = enumerateStructuralCandidates(graphStore, delta, { publicOnly: true, judgment: decision });
57
+ const list = byCommit.get(meta.sha) ?? [];
58
+ list.push({
59
+ decision_id: decision.id,
60
+ candidate_ids: new Set(inspection.candidates.map((candidate) => candidate.id)),
61
+ exact: inspection.candidates.length === 1,
62
+ });
63
+ byCommit.set(meta.sha, list);
64
+ }
65
+ catch {
66
+ // A human decision can be real while still lacking a supported structural
67
+ // binding. It contributes no candidate attestation rather than being
68
+ // stretched over a coincidental fact from the same commit.
69
+ }
70
+ }
71
+ return byCommit;
72
+ }
73
+ /** Read-only review packet. Structural facts propose corpus pairs but cannot
74
+ * become policy evidence until an exact human judgment selects their semantics. */
75
+ export function buildG2CandidateReview(store, root, opts = {}, resolutions = []) {
76
+ const scratchRoot = mkdtempSync(join(tmpdir(), "hunch-g2-candidates-"));
77
+ const graphStore = new HunchStore(hunchPathsForDir(scratchRoot));
78
+ try {
79
+ graphStore.json.ensureDirs();
80
+ indexRepo(graphStore, root, { churn: false });
81
+ return buildFromIndexedGraph(store, graphStore, root, opts, resolutions);
82
+ }
83
+ finally {
84
+ graphStore.close();
85
+ rmSync(scratchRoot, { recursive: true, force: true });
86
+ }
87
+ }
88
+ function buildFromIndexedGraph(decisionStore, graphStore, root, opts, resolutions) {
89
+ const since = (opts.since ?? "180d").trim();
90
+ if (!since || since.length > 100)
91
+ throw new Error("G2 candidate since window must be a non-empty bounded string");
92
+ const maxCommits = positiveBound(opts.maxCommits ?? 100, "G2 candidate maxCommits", 200);
93
+ const limit = positiveBound(opts.limit ?? 30, "G2 candidate limit", 100);
94
+ const commits = fixCommits(since, root, maxCommits);
95
+ const grounding = privateGrounding(decisionStore, graphStore, root);
96
+ const failures = [];
97
+ const candidates = [];
98
+ let candidateCommits = 0;
99
+ for (const commit of commits) {
100
+ try {
101
+ const meta = commitMeta(commit, root);
102
+ if (!meta)
103
+ throw new Error("commit metadata is unavailable");
104
+ const delta = extractStructuralDelta(root, commit);
105
+ // The private overlay supplies human intent only. Symbols and edges are a
106
+ // derived view of public source and may be stale in long-lived overlays,
107
+ // so bindings must come from the freshly indexed public graph alone.
108
+ const enumerated = enumerateStructuralCandidates(graphStore, delta, { publicOnly: true });
109
+ if (enumerated.candidates.length)
110
+ candidateCommits++;
111
+ for (const candidate of enumerated.candidates) {
112
+ const matches = (grounding.get(meta.sha) ?? []).filter((entry) => entry.candidate_ids.has(candidate.id));
113
+ const status = matches.some((entry) => entry.exact)
114
+ ? "human_grounded_exact"
115
+ : matches.length
116
+ ? "human_grounded_needs_selection"
117
+ : "unattested_structural_coincidence";
118
+ const seed = canonicalHash({ commit: meta.sha, candidate_id: candidate.id });
119
+ candidates.push({
120
+ id: `g2candidate_${shortHash(seed)}`,
121
+ candidate_id: candidate.id,
122
+ commit: meta.sha,
123
+ commit_subject: meta.subject,
124
+ commit_date: meta.date,
125
+ changed_files: [...meta.files].sort(),
126
+ sibling_candidates: enumerated.candidates.length,
127
+ basis: candidate.basis,
128
+ reason: candidate.reason,
129
+ assertion: candidate.assertion,
130
+ scope: candidate.scope,
131
+ proposed_corpus: {
132
+ known_bad: { ref: delta.before_commit, expected: "violated" },
133
+ known_good: { ref: delta.after_commit, expected: "satisfied" },
134
+ observed: false,
135
+ },
136
+ attestation: {
137
+ status,
138
+ decision_ids: [...new Set(matches.map((entry) => entry.decision_id))].sort(),
139
+ },
140
+ human_review: null,
141
+ });
142
+ }
143
+ }
144
+ catch (error) {
145
+ failures.push({ commit, error: error.message });
146
+ }
147
+ }
148
+ for (const candidate of candidates) {
149
+ const candidateHash = g2CandidateItemHash(candidate);
150
+ candidate.human_review = resolutions.find((resolution) => resolution.candidate_id === candidate.id && resolution.candidate_hash === candidateHash) ?? null;
151
+ }
152
+ candidates.sort((left, right) => {
153
+ const attestation = attestationRank[left.attestation.status] - attestationRank[right.attestation.status];
154
+ if (attestation)
155
+ return attestation;
156
+ const leftSource = left.scope.paths.some((path) => path.startsWith("src/")) ? 0 : 1;
157
+ const rightSource = right.scope.paths.some((path) => path.startsWith("src/")) ? 0 : 1;
158
+ if (leftSource !== rightSource)
159
+ return leftSource - rightSource;
160
+ const basis = basisRank[left.basis] - basisRank[right.basis];
161
+ if (basis)
162
+ return basis;
163
+ return right.commit_date.localeCompare(left.commit_date)
164
+ || left.commit.localeCompare(right.commit)
165
+ || left.candidate_id.localeCompare(right.candidate_id);
166
+ });
167
+ const counts = {
168
+ human_grounded_exact: candidates.filter((candidate) => candidate.attestation.status === "human_grounded_exact").length,
169
+ human_grounded_needs_selection: candidates.filter((candidate) => candidate.attestation.status === "human_grounded_needs_selection").length,
170
+ unattested_structural_coincidence: candidates.filter((candidate) => candidate.attestation.status === "unattested_structural_coincidence").length,
171
+ selected_candidates: candidates.filter((candidate) => candidate.human_review?.disposition === "selected").length,
172
+ rejected_candidates: candidates.filter((candidate) => candidate.human_review?.disposition === "rejected").length,
173
+ unreviewed_candidates: candidates.filter((candidate) => candidate.human_review === null).length,
174
+ };
175
+ const items = candidates.slice(0, limit);
176
+ const body = {
177
+ since,
178
+ max_commits: maxCommits,
179
+ limit,
180
+ scanned_fix_commits: commits.length,
181
+ candidate_commits: candidateCommits,
182
+ total_candidates: candidates.length,
183
+ ...counts,
184
+ extraction_failures: failures.sort((left, right) => left.commit.localeCompare(right.commit)),
185
+ items,
186
+ has_more: candidates.length > items.length,
187
+ data_class: "private",
188
+ authority: "none",
189
+ writes: "none",
190
+ proof_status: "not_run",
191
+ };
192
+ const contentHash = canonicalHash(body);
193
+ return { id: `g2candidates_${shortHash(contentHash)}`, content_hash: contentHash, ...body };
194
+ }
195
+ //# sourceMappingURL=g2Candidates.js.map