@davesheffer/hunch 1.7.0 → 1.8.0

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 (71) hide show
  1. package/README.md +242 -0
  2. package/bench/constitution-exp03-v1.json +70 -0
  3. package/dist/cli/index.js +1630 -107
  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 +1007 -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 +132 -0
  31. package/dist/constitution/lifecycle.js +224 -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/repairPolicies.js +78 -0
  38. package/dist/constitution/replay.js +361 -0
  39. package/dist/constitution/replayCache.js +89 -0
  40. package/dist/constitution/replayWorker.js +34 -0
  41. package/dist/constitution/repository.js +533 -0
  42. package/dist/constitution/schema.js +545 -0
  43. package/dist/constitution/scorecard.js +106 -0
  44. package/dist/constitution/service.js +1211 -0
  45. package/dist/constitution/shadow.js +235 -0
  46. package/dist/constitution/sourceMutation.js +316 -0
  47. package/dist/constitution/structural.js +601 -0
  48. package/dist/core/autoreview.js +27 -3
  49. package/dist/core/dupdetect.js +10 -3
  50. package/dist/core/escalations.js +65 -0
  51. package/dist/core/events.js +61 -0
  52. package/dist/core/externalImports.js +24 -0
  53. package/dist/core/hookpolicy.js +3 -0
  54. package/dist/core/memorylog.js +69 -0
  55. package/dist/core/relativeImports.js +33 -0
  56. package/dist/core/repair.js +71 -0
  57. package/dist/core/reviewqueue.js +11 -0
  58. package/dist/core/stats.js +115 -0
  59. package/dist/extractors/git.js +120 -0
  60. package/dist/extractors/indexer.js +39 -38
  61. package/dist/extractors/nativeTreeSitter.js +108 -0
  62. package/dist/extractors/parse.js +5 -15
  63. package/dist/integrations/claudemd.js +8 -1
  64. package/dist/integrations/gitignore.js +8 -0
  65. package/dist/integrations/providers.js +32 -10
  66. package/dist/integrations/sync.js +16 -1
  67. package/dist/mcp/server.js +317 -1
  68. package/dist/synthesis/synthesize.js +8 -1
  69. package/dist/wiki/graph.js +301 -0
  70. package/dist/wiki/wiki.js +31 -3
  71. package/package.json +5 -1
@@ -0,0 +1,154 @@
1
+ import { shortHash } from "../core/ids.js";
2
+ import { commitChanges, fileAtRef, firstParent, revParse } from "../extractors/git.js";
3
+ import { attributeCalls, parseSource } from "../extractors/parse.js";
4
+ import { canonicalHash } from "./canonical.js";
5
+ import { StructuralDeltaSchema, } from "./schema.js";
6
+ const CODE_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
7
+ const SKIP_SEGMENTS = new Set(["node_modules", ".git", ".hunch", "dist", "build", "coverage", ".next", "out", "vendor"]);
8
+ const MAX_CODE_FILES = 64;
9
+ const MAX_SOURCE_BYTES = 8 * 1024 * 1024;
10
+ const MAX_FACTS = 128;
11
+ function eligibleCode(file) {
12
+ return !!file
13
+ && CODE_EXT.test(file)
14
+ && !file.includes(".generated.")
15
+ && !file.split(/[\\/]/).some((segment) => SKIP_SEGMENTS.has(segment));
16
+ }
17
+ const symbolKey = (s) => `${s.kind}\0${s.name}`;
18
+ const callKey = (c) => `${c.caller}\0${c.callee}\0${c.member ? "1" : "0"}`;
19
+ function view(file, source) {
20
+ if (source == null || !eligibleCode(file))
21
+ return null;
22
+ const parsed = parseSource(file, source);
23
+ if (!parsed)
24
+ return null;
25
+ return viewOfParsed(parsed);
26
+ }
27
+ function viewOfParsed(parsed) {
28
+ const symbols = new Map(parsed.symbols.map((s) => [symbolKey(s), s]));
29
+ const byStart = new Map(parsed.symbols.map((s) => [s.startByte, s]));
30
+ const calls = new Map();
31
+ for (const [start, callees] of attributeCalls(parsed)) {
32
+ const caller = byStart.get(start);
33
+ if (!caller)
34
+ continue;
35
+ for (const [callee, member] of callees) {
36
+ const call = { caller: caller.name, callee, member };
37
+ calls.set(callKey(call), call);
38
+ }
39
+ }
40
+ return { symbols, calls, imports: new Set(parsed.imports) };
41
+ }
42
+ function symbolRef(file, symbol) {
43
+ return { file, name: symbol.name, kind: symbol.kind };
44
+ }
45
+ function callRef(file, call) {
46
+ return { file, ...call };
47
+ }
48
+ function importRef(file, specifier) {
49
+ return { file, specifier };
50
+ }
51
+ function sortByKey(items, key) {
52
+ return items.sort((a, b) => key(a).localeCompare(key(b)));
53
+ }
54
+ /** Compare exact git blobs at a commit and its first parent. No checkout,
55
+ * worktree, hook, or model/provider is involved. */
56
+ export function extractStructuralDelta(root, commit) {
57
+ const after = revParse(commit, root);
58
+ const before = firstParent(after, root);
59
+ if (!before)
60
+ throw new Error(`commit ${commit} has no first parent; an initial snapshot cannot prove a fixing structural delta`);
61
+ const addedSymbols = [];
62
+ const removedSymbols = [];
63
+ const movedSymbols = [];
64
+ const addedCalls = [];
65
+ const removedCalls = [];
66
+ const addedImports = [];
67
+ const removedImports = [];
68
+ const files = new Set();
69
+ let codeFiles = 0;
70
+ let sourceBytes = 0;
71
+ for (const change of commitChanges(after, root)) {
72
+ const beforeFile = change.before;
73
+ const afterFile = change.after;
74
+ if (beforeFile)
75
+ files.add(beforeFile);
76
+ if (afterFile)
77
+ files.add(afterFile);
78
+ if (!eligibleCode(beforeFile) && !eligibleCode(afterFile))
79
+ continue;
80
+ if (++codeFiles > MAX_CODE_FILES)
81
+ throw new Error(`structural delta exceeds ${MAX_CODE_FILES} changed code files; narrow or split the evidence commit`);
82
+ const beforeSource = change.status === "copied" || !eligibleCode(beforeFile) ? null : fileAtRef(before, beforeFile, root);
83
+ const afterSource = !eligibleCode(afterFile) ? null : fileAtRef(after, afterFile, root);
84
+ sourceBytes += Buffer.byteLength(beforeSource ?? "") + Buffer.byteLength(afterSource ?? "");
85
+ if (sourceBytes > MAX_SOURCE_BYTES)
86
+ throw new Error(`structural delta exceeds ${MAX_SOURCE_BYTES} source bytes; narrow or split the evidence commit`);
87
+ const beforeView = beforeFile ? view(beforeFile, beforeSource) : null;
88
+ const afterView = afterFile ? view(afterFile, afterSource) : null;
89
+ if (!beforeView && !afterView)
90
+ continue;
91
+ const beforeSymbols = beforeView?.symbols ?? new Map();
92
+ const afterSymbols = afterView?.symbols ?? new Map();
93
+ for (const [key, symbol] of afterSymbols) {
94
+ if (!beforeSymbols.has(key))
95
+ addedSymbols.push(symbolRef(afterFile, symbol));
96
+ else if (change.status === "renamed" && beforeFile !== afterFile) {
97
+ movedSymbols.push({ from: beforeFile, to: afterFile, name: symbol.name, kind: symbol.kind });
98
+ }
99
+ }
100
+ for (const [key, symbol] of beforeSymbols) {
101
+ if (!afterSymbols.has(key))
102
+ removedSymbols.push(symbolRef(beforeFile, symbol));
103
+ }
104
+ const beforeCalls = beforeView?.calls ?? new Map();
105
+ const afterCalls = afterView?.calls ?? new Map();
106
+ for (const [key, call] of afterCalls) {
107
+ if (!beforeCalls.has(key))
108
+ addedCalls.push(callRef(afterFile, call));
109
+ }
110
+ for (const [key, call] of beforeCalls) {
111
+ if (!afterCalls.has(key))
112
+ removedCalls.push(callRef(afterFile ?? beforeFile, call));
113
+ }
114
+ const beforeImports = beforeView?.imports ?? new Set();
115
+ const afterImports = afterView?.imports ?? new Set();
116
+ for (const specifier of afterImports) {
117
+ if (!beforeImports.has(specifier))
118
+ addedImports.push(importRef(afterFile, specifier));
119
+ }
120
+ for (const specifier of beforeImports) {
121
+ if (!afterImports.has(specifier))
122
+ removedImports.push(importRef(afterFile ?? beforeFile, specifier));
123
+ }
124
+ }
125
+ const factCount = addedSymbols.length + removedSymbols.length + movedSymbols.length
126
+ + addedCalls.length + removedCalls.length + addedImports.length + removedImports.length;
127
+ if (factCount > MAX_FACTS)
128
+ throw new Error(`structural delta exceeds ${MAX_FACTS} extracted facts; narrow or split the evidence commit`);
129
+ const body = {
130
+ before_commit: before,
131
+ after_commit: after,
132
+ files: [...files].sort(),
133
+ symbols: {
134
+ added: sortByKey(addedSymbols, (s) => `${s.file}\0${s.kind}\0${s.name}`),
135
+ removed: sortByKey(removedSymbols, (s) => `${s.file}\0${s.kind}\0${s.name}`),
136
+ moved: sortByKey(movedSymbols, (s) => `${s.from}\0${s.to}\0${s.kind}\0${s.name}`),
137
+ },
138
+ calls: {
139
+ added: sortByKey(addedCalls, (c) => `${c.file}\0${callKey(c)}`),
140
+ removed: sortByKey(removedCalls, (c) => `${c.file}\0${callKey(c)}`),
141
+ },
142
+ imports: {
143
+ added: sortByKey(addedImports, (i) => `${i.file}\0${i.specifier}`),
144
+ removed: sortByKey(removedImports, (i) => `${i.file}\0${i.specifier}`),
145
+ },
146
+ };
147
+ const contentHash = canonicalHash(body);
148
+ return StructuralDeltaSchema.parse({
149
+ id: `delta_${shortHash(contentHash)}`,
150
+ ...body,
151
+ content_hash: contentHash,
152
+ });
153
+ }
154
+ //# sourceMappingURL=delta.js.map
@@ -0,0 +1,141 @@
1
+ import { shortHash } from "../core/ids.js";
2
+ import { canonicalHash } from "./canonical.js";
3
+ import { policyProofHash } from "./composition.js";
4
+ import { HistoryDispositionClassificationSchema, HistoryDispositionSchema, } from "./schema.js";
5
+ export function historyDispositionContentHash(disposition) {
6
+ const { id: _id, content_hash: _contentHash, ...body } = disposition;
7
+ return canonicalHash(body);
8
+ }
9
+ export function historyDispositionJudgmentHash(disposition) {
10
+ const { id: _id, content_hash: _contentHash, created_at: _createdAt, ...judgment } = disposition;
11
+ return canonicalHash(judgment);
12
+ }
13
+ export function compileHistoryDisposition(policy, proof, receipt, classification, actor, reason, opts = {}) {
14
+ const parsedClassification = HistoryDispositionClassificationSchema.parse(classification);
15
+ const policyHash = policyProofHash(policy, opts.composition ?? []);
16
+ if (policy.proof !== proof.id)
17
+ throw new Error(`policy ${policy.id} does not link proof ${proof.id}`);
18
+ if (proof.policy_hash !== policyHash)
19
+ throw new Error(`proof ${proof.id} does not match current policy semantics`);
20
+ if (proof.data_class !== policy.data_class)
21
+ throw new Error(`proof ${proof.id} does not match policy ${policy.id} data class`);
22
+ if (receipt.leg !== "accepted_history" || receipt.result !== "violated") {
23
+ throw new Error("history dispositions apply only to violated accepted-history replay receipts");
24
+ }
25
+ if (receipt.policy_hash !== proof.policy_hash)
26
+ throw new Error(`receipt ${receipt.deterministic_hash} does not match proof policy semantics`);
27
+ if (!proof.replay_receipts.some((candidate) => candidate.leg === receipt.leg && candidate.commit === receipt.commit && candidate.result === receipt.result && candidate.deterministic_hash === receipt.deterministic_hash)) {
28
+ throw new Error(`receipt ${receipt.deterministic_hash} is not embedded in proof ${proof.id}`);
29
+ }
30
+ const createdAt = opts.now ?? new Date().toISOString();
31
+ const body = {
32
+ policy_id: policy.id,
33
+ proof_id: proof.id,
34
+ policy_hash: proof.policy_hash,
35
+ plan_hash: proof.plan_hash,
36
+ commit: receipt.commit,
37
+ receipt_hash: receipt.deterministic_hash,
38
+ classification: parsedClassification,
39
+ actor,
40
+ reason: reason.trim(),
41
+ supersedes: opts.supersedes ?? null,
42
+ data_class: policy.data_class,
43
+ created_at: createdAt,
44
+ };
45
+ const contentHash = canonicalHash(body);
46
+ return HistoryDispositionSchema.parse({
47
+ id: `disp_${shortHash(contentHash)}`,
48
+ content_hash: contentHash,
49
+ ...body,
50
+ });
51
+ }
52
+ /** Resolve the append-only supersession chain without trusting timestamps.
53
+ * Missing parents, cross-hit links, branches, and cycles fail visibly. */
54
+ export function currentHistoryDispositions(records) {
55
+ const byId = new Map(records.map((record) => [record.id, record]));
56
+ if (byId.size !== records.length)
57
+ throw new Error("duplicate history disposition id");
58
+ const childCount = new Map();
59
+ for (const record of records) {
60
+ if (!record.supersedes)
61
+ continue;
62
+ const parent = byId.get(record.supersedes);
63
+ if (!parent)
64
+ throw new Error(`history disposition ${record.id} supersedes missing ${record.supersedes}`);
65
+ if (parent.policy_id !== record.policy_id || parent.proof_id !== record.proof_id || parent.commit !== record.commit) {
66
+ throw new Error(`history disposition ${record.id} supersedes a different policy/proof/commit hit`);
67
+ }
68
+ childCount.set(parent.id, (childCount.get(parent.id) ?? 0) + 1);
69
+ if (childCount.get(parent.id) > 1)
70
+ throw new Error(`history disposition ${parent.id} has a branched supersession chain`);
71
+ }
72
+ for (const record of records) {
73
+ const visited = new Set();
74
+ let cursor = record;
75
+ while (cursor?.supersedes) {
76
+ if (visited.has(cursor.id))
77
+ throw new Error(`history disposition chain contains a cycle at ${cursor.id}`);
78
+ visited.add(cursor.id);
79
+ cursor = byId.get(cursor.supersedes);
80
+ }
81
+ }
82
+ const current = records
83
+ .filter((record) => !childCount.has(record.id))
84
+ .sort((left, right) => left.policy_id.localeCompare(right.policy_id) || left.proof_id.localeCompare(right.proof_id) || left.commit.localeCompare(right.commit));
85
+ const currentByHit = new Set();
86
+ for (const record of current) {
87
+ const key = `${record.policy_id}:${record.proof_id}:${record.commit}`;
88
+ if (currentByHit.has(key))
89
+ throw new Error(`history hit ${record.commit} has multiple current dispositions`);
90
+ currentByHit.add(key);
91
+ }
92
+ return current;
93
+ }
94
+ export function assessHistoryDispositions(proof, records) {
95
+ const current = currentHistoryDispositions(records).filter((record) => record.proof_id === proof.id);
96
+ const violations = proof.replay_receipts.filter((receipt) => receipt.leg === "accepted_history" && receipt.result === "violated");
97
+ const counts = {
98
+ true_positive_actionable: 0,
99
+ true_positive_accepted_exception: 0,
100
+ false_positive_selector: 0,
101
+ false_positive_semantics: 0,
102
+ false_positive_stale: 0,
103
+ unknown_insufficient_parser: 0,
104
+ };
105
+ if (violations.length !== proof.accepted_history.violated) {
106
+ return {
107
+ current,
108
+ bound: [],
109
+ missing_commits: violations.map((receipt) => receipt.commit),
110
+ unresolved_count: proof.accepted_history.violated,
111
+ counts,
112
+ blocking_error: "blocking proof accepted-history violation summary does not match its replay receipts",
113
+ };
114
+ }
115
+ const bound = [];
116
+ const missing = [];
117
+ for (const receipt of violations) {
118
+ const candidates = current.filter((record) => record.commit === receipt.commit);
119
+ const disposition = candidates.find((record) => record.receipt_hash === receipt.deterministic_hash && record.policy_hash === proof.policy_hash && record.plan_hash === proof.plan_hash);
120
+ if (!disposition) {
121
+ missing.push(receipt.commit);
122
+ continue;
123
+ }
124
+ bound.push(disposition);
125
+ counts[disposition.classification] += 1;
126
+ }
127
+ let blockingError = null;
128
+ if (missing.length)
129
+ blockingError = "blocking proof has unclassified accepted-history violation hits";
130
+ else if (counts.false_positive_selector + counts.false_positive_semantics + counts.false_positive_stale > 0) {
131
+ blockingError = "blocking proof has a human-classified accepted-history false positive; repair and re-prove the policy first";
132
+ }
133
+ else if (counts.unknown_insufficient_parser > 0) {
134
+ blockingError = "blocking proof has an accepted-history hit classified unknown for insufficient parser support";
135
+ }
136
+ else if (counts.true_positive_accepted_exception > 0) {
137
+ blockingError = "blocking proof has an accepted exception that requires separately proved parent/exception composition";
138
+ }
139
+ return { current, bound, missing_commits: missing, unresolved_count: missing.length, counts, blocking_error: blockingError };
140
+ }
141
+ //# sourceMappingURL=disposition.js.map