@davesheffer/hunch 1.22.3 → 1.23.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.
@@ -0,0 +1,253 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { compareCodeUnits } from "./canonicalOrder.js";
4
+ export const CHANGE_PROOF_SCHEMA_VERSION = "hunch.change-proof/1";
5
+ export const CHANGE_PROOF_ALGORITHM = "hunch-change-proof-sha256/1";
6
+ export const CHANGE_PROOF_VERDICTS = ["fail", "pass", "unknown"];
7
+ export const CHANGE_PROOF_RELEVANCE = ["blast_radius", "changed_path", "conformance", "guard"];
8
+ const GIT_OBJECT = /^[a-f0-9]{40,64}$/;
9
+ const SHA1 = /^sha1:[a-f0-9]{40}$/;
10
+ const SHA256 = /^sha256:[a-f0-9]{64}$/;
11
+ const CHANGE_ID = /^hchg_[a-f0-9]{24}$/;
12
+ const PROOF_ID = /^hproof_[a-f0-9]{24}$/;
13
+ const PROFILE_ID = /^pdna_[a-f0-9]{24}$/;
14
+ const REPOSITORY_ID = /^pdnar_[a-f0-9]{24}$/;
15
+ const RepoPathSchema = z.string().min(1).max(4_096).refine((path) => {
16
+ if (path.includes("\0") || path.includes("\\") || path.startsWith("/") || /^[A-Za-z]:/.test(path))
17
+ return false;
18
+ return !path.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
19
+ }, "repository path must be canonical and relative");
20
+ const HashGapSchema = z.object({
21
+ code: z.string().regex(/^[a-z][a-z0-9_.-]{2,127}$/),
22
+ count: z.number().int().positive().max(1_000_000),
23
+ evidence_hash: z.string().regex(SHA256),
24
+ }).strict();
25
+ const ChangeIdentitySchema = z.object({
26
+ schema: z.literal("hunch.change-identity/1"),
27
+ algorithm: z.literal("git-raw-tree-delta-sha256/1"),
28
+ change_id: z.string().regex(CHANGE_ID),
29
+ base_revision: z.string().regex(GIT_OBJECT),
30
+ head_revision: z.string().regex(GIT_OBJECT),
31
+ base_tree: z.string().regex(GIT_OBJECT),
32
+ head_tree: z.string().regex(GIT_OBJECT),
33
+ delta_hash: z.string().regex(SHA256),
34
+ patch_id: z.string().regex(GIT_OBJECT).nullable(),
35
+ file_count: z.number().int().positive().max(16_384),
36
+ paths_hash: z.string().regex(SHA256),
37
+ content_hash: z.string().regex(SHA256),
38
+ }).strict();
39
+ const GraphSealSchema = z.object({
40
+ source: z.literal("commit"),
41
+ revision: z.string().regex(GIT_OBJECT),
42
+ source_hash: z.string().regex(SHA1),
43
+ topology_hash: z.string().regex(SHA256),
44
+ files: z.number().int().nonnegative().max(1_000_000),
45
+ symbols: z.number().int().nonnegative().max(10_000_000),
46
+ edges: z.number().int().nonnegative().max(50_000_000),
47
+ components: z.number().int().nonnegative().max(1_000_000),
48
+ issue_count: z.number().int().nonnegative().max(1_000_000),
49
+ }).strict();
50
+ const BlastEntrySchema = z.object({
51
+ source_path: RepoPathSchema,
52
+ dependent_path: RepoPathSchema,
53
+ depth: z.number().int().min(1).max(4),
54
+ graphs: z.array(z.enum(["base", "result"])).min(1).max(2),
55
+ }).strict();
56
+ const DecisionRefSchema = z.object({
57
+ id: z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/),
58
+ record_hash: z.string().regex(SHA256),
59
+ relevance: z.array(z.enum(CHANGE_PROOF_RELEVANCE)).min(1).max(4),
60
+ paths: z.array(RepoPathSchema).max(128),
61
+ path_count: z.number().int().nonnegative().max(1_000_000),
62
+ paths_hash: z.string().regex(SHA256),
63
+ }).strict();
64
+ const ConstraintRefSchema = z.object({
65
+ id: z.string().regex(/^con_[A-Za-z0-9_.-]{3,}$/),
66
+ record_hash: z.string().regex(SHA256),
67
+ severity: z.enum(["advisory", "warning", "blocking"]),
68
+ relevance: z.array(z.enum(["blast_radius", "changed_path"])).min(1).max(2),
69
+ paths: z.array(RepoPathSchema).max(128),
70
+ path_count: z.number().int().nonnegative().max(1_000_000),
71
+ paths_hash: z.string().regex(SHA256),
72
+ }).strict();
73
+ const ConformanceReceiptSchema = z.object({
74
+ decision_id: z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/),
75
+ predicate_index: z.number().int().nonnegative().max(1_000_000),
76
+ predicate_hash: z.string().regex(SHA256),
77
+ satisfied: z.boolean(),
78
+ detail_hash: z.string().regex(SHA256),
79
+ }).strict();
80
+ export const ChangeProofSchema = z.object({
81
+ schema: z.literal(CHANGE_PROOF_SCHEMA_VERSION),
82
+ algorithm: z.literal(CHANGE_PROOF_ALGORITHM),
83
+ proof_id: z.string().regex(PROOF_ID),
84
+ engine: z.object({
85
+ package: z.literal("@davesheffer/hunch"),
86
+ version: z.string().min(1).max(64),
87
+ }).strict(),
88
+ repository: z.object({
89
+ repository_id: z.string().regex(REPOSITORY_ID),
90
+ base_revision: z.string().regex(GIT_OBJECT),
91
+ result_revision: z.string().regex(GIT_OBJECT),
92
+ }).strict(),
93
+ change: ChangeIdentitySchema,
94
+ project_dna: z.object({
95
+ schema: z.literal("hunch.project-dna/1"),
96
+ profile_id: z.string().regex(PROFILE_ID),
97
+ repository_id: z.string().regex(REPOSITORY_ID),
98
+ repository_revision: z.string().regex(GIT_OBJECT),
99
+ content_hash: z.string().regex(SHA256),
100
+ trait_ids: z.array(z.string().regex(/^pdnat_[a-f0-9]{20}$/)).max(64),
101
+ }).strict(),
102
+ graph: z.object({
103
+ base: GraphSealSchema,
104
+ result: GraphSealSchema,
105
+ }).strict(),
106
+ changed_files: z.array(RepoPathSchema).max(2_048),
107
+ changed_file_count: z.number().int().positive().max(16_384),
108
+ blast_radius: z.array(BlastEntrySchema).max(4_096),
109
+ blast_radius_count: z.number().int().nonnegative().max(10_000_000),
110
+ decisions: z.array(DecisionRefSchema).max(1_024),
111
+ decision_count: z.number().int().nonnegative().max(1_000_000),
112
+ constraints: z.array(ConstraintRefSchema).max(1_024),
113
+ constraint_count: z.number().int().nonnegative().max(1_000_000),
114
+ conformance: z.array(ConformanceReceiptSchema).max(1_024),
115
+ conformance_count: z.number().int().nonnegative().max(1_000_000),
116
+ guard: z.object({
117
+ verdict: z.enum(["pass", "fail"]),
118
+ strict_blocker_ids: z.array(z.string().min(1).max(256)).max(2_048),
119
+ regression_decision_ids: z.array(z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/)).max(1_024),
120
+ veto_decision_ids: z.array(z.string().regex(/^dec_[A-Za-z0-9_.-]{3,}$/)).max(1_024),
121
+ report_hash: z.string().regex(SHA256),
122
+ }).strict(),
123
+ memory: z.object({
124
+ scope: z.enum(["public", "union"]),
125
+ records_hash: z.string().regex(SHA256),
126
+ }).strict(),
127
+ omissions: z.array(HashGapSchema).max(64),
128
+ unknowns: z.array(HashGapSchema).max(64),
129
+ verdict: z.enum(CHANGE_PROOF_VERDICTS),
130
+ authority: z.object({
131
+ execution: z.literal(false),
132
+ ci: z.literal(false),
133
+ deployment: z.literal(false),
134
+ merge: z.literal(false),
135
+ ranking: z.literal(false),
136
+ promotion: z.literal(false),
137
+ policy: z.literal(false),
138
+ }).strict(),
139
+ content_hash: z.string().regex(SHA256),
140
+ }).strict();
141
+ export function canonicalChangeProofJson(value) {
142
+ if (Array.isArray(value))
143
+ return `[${value.map(canonicalChangeProofJson).join(",")}]`;
144
+ if (value && typeof value === "object") {
145
+ return `{${Object.entries(value)
146
+ .filter(([, child]) => child !== undefined)
147
+ .sort(([left], [right]) => compareCodeUnits(left, right))
148
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonicalChangeProofJson(child)}`)
149
+ .join(",")}}`;
150
+ }
151
+ return JSON.stringify(value) ?? "null";
152
+ }
153
+ export function changeProofHash(value) {
154
+ return `sha256:${createHash("sha256").update(canonicalChangeProofJson(value)).digest("hex")}`;
155
+ }
156
+ function canonicalArray(values) {
157
+ const rendered = values.map(canonicalChangeProofJson);
158
+ return rendered.every((value, index) => index === 0 || compareCodeUnits(rendered[index - 1], value) < 0);
159
+ }
160
+ function expectedChangeId(change) {
161
+ return `hchg_${changeProofHash({ algorithm: change.algorithm, delta_hash: change.delta_hash })
162
+ .slice("sha256:".length, "sha256:".length + 24)}`;
163
+ }
164
+ export function sealChangeProof(unsigned) {
165
+ const proofId = `hproof_${changeProofHash(unsigned).slice("sha256:".length, "sha256:".length + 24)}`;
166
+ const sealed = { ...unsigned, proof_id: proofId };
167
+ const proof = { ...sealed, content_hash: changeProofHash(sealed) };
168
+ assertChangeProof(proof);
169
+ return proof;
170
+ }
171
+ export function assertChangeProof(value) {
172
+ const proof = ChangeProofSchema.parse(value);
173
+ const sortedArrays = [
174
+ proof.changed_files,
175
+ proof.blast_radius,
176
+ proof.decisions,
177
+ proof.constraints,
178
+ proof.conformance,
179
+ proof.guard.strict_blocker_ids,
180
+ proof.guard.regression_decision_ids,
181
+ proof.guard.veto_decision_ids,
182
+ proof.omissions,
183
+ proof.unknowns,
184
+ proof.project_dna.trait_ids,
185
+ ];
186
+ if (sortedArrays.some((items) => !canonicalArray(items)))
187
+ throw new Error("change proof collections must be unique and canonically ordered");
188
+ for (const decision of proof.decisions) {
189
+ if (!canonicalArray(decision.relevance) || !canonicalArray(decision.paths)
190
+ || decision.path_count < decision.paths.length
191
+ || (decision.path_count === decision.paths.length && decision.paths_hash !== changeProofHash(decision.paths))) {
192
+ throw new Error("change proof decision reference is non-canonical");
193
+ }
194
+ }
195
+ for (const constraint of proof.constraints) {
196
+ if (!canonicalArray(constraint.relevance) || !canonicalArray(constraint.paths)
197
+ || constraint.path_count < constraint.paths.length
198
+ || (constraint.path_count === constraint.paths.length && constraint.paths_hash !== changeProofHash(constraint.paths))) {
199
+ throw new Error("change proof constraint reference is non-canonical");
200
+ }
201
+ }
202
+ for (const blast of proof.blast_radius) {
203
+ if (!canonicalArray(blast.graphs) || blast.source_path === blast.dependent_path) {
204
+ throw new Error("change proof blast radius is non-canonical");
205
+ }
206
+ }
207
+ const conformanceKeys = new Set();
208
+ for (const receipt of proof.conformance) {
209
+ const key = `${receipt.decision_id}\0${receipt.predicate_index}`;
210
+ if (conformanceKeys.has(key))
211
+ throw new Error("change proof conformance predicate identity is duplicated");
212
+ conformanceKeys.add(key);
213
+ }
214
+ const changeUnsigned = (({ content_hash: _contentHash, ...rest }) => rest)(proof.change);
215
+ if (proof.change.change_id !== expectedChangeId(proof.change)
216
+ || proof.change.content_hash !== changeProofHash(changeUnsigned)) {
217
+ throw new Error("change proof change identity seal is invalid");
218
+ }
219
+ if (proof.repository.base_revision !== proof.change.base_revision
220
+ || proof.repository.result_revision !== proof.change.head_revision
221
+ || proof.project_dna.repository_id !== proof.repository.repository_id
222
+ || proof.project_dna.repository_revision !== proof.repository.result_revision
223
+ || proof.graph.base.revision !== proof.repository.base_revision
224
+ || proof.graph.result.revision !== proof.repository.result_revision
225
+ || proof.changed_file_count !== proof.change.file_count
226
+ || proof.changed_file_count < proof.changed_files.length
227
+ || proof.blast_radius_count < proof.blast_radius.length
228
+ || proof.decision_count < proof.decisions.length
229
+ || proof.constraint_count < proof.constraints.length
230
+ || proof.conformance_count < proof.conformance.length) {
231
+ throw new Error("change proof exact-revision or count binding is invalid");
232
+ }
233
+ const recordsHash = changeProofHash({ decisions: proof.decisions, constraints: proof.constraints });
234
+ if (proof.memory.records_hash !== recordsHash)
235
+ throw new Error("change proof memory seal is invalid");
236
+ const guardFails = proof.guard.strict_blocker_ids.length > 0;
237
+ if (proof.guard.verdict !== (guardFails ? "fail" : "pass"))
238
+ throw new Error("change proof guard verdict is invalid");
239
+ const expectedVerdict = guardFails || proof.conformance.some((receipt) => !receipt.satisfied)
240
+ ? "fail"
241
+ : proof.omissions.length || proof.unknowns.length
242
+ ? "unknown"
243
+ : "pass";
244
+ if (proof.verdict !== expectedVerdict)
245
+ throw new Error("change proof verdict is invalid");
246
+ const { proof_id: _proofId, content_hash: _proofHash, ...unsigned } = proof;
247
+ const expectedProofId = `hproof_${changeProofHash(unsigned).slice("sha256:".length, "sha256:".length + 24)}`;
248
+ const sealed = { ...unsigned, proof_id: proof.proof_id };
249
+ if (proof.proof_id !== expectedProofId || proof.content_hash !== changeProofHash(sealed)) {
250
+ throw new Error("change proof seal is invalid");
251
+ }
252
+ }
253
+ //# sourceMappingURL=changeProofContract.js.map
@@ -13,8 +13,8 @@ export declare const ProjectDnaUsefulnessObservationSchema: z.ZodObject<{
13
13
  episodeHash: z.ZodString;
14
14
  terminalAt: z.ZodString;
15
15
  result: z.ZodEnum<{
16
- pass: "pass";
17
16
  fail: "fail";
17
+ pass: "pass";
18
18
  uncertain: "uncertain";
19
19
  abandoned: "abandoned";
20
20
  rolled_back: "rolled_back";
@@ -44,10 +44,10 @@ export declare const ProjectDnaUsefulnessObservationSchema: z.ZodObject<{
44
44
  }, z.core.$strict>;
45
45
  artifact: z.ZodObject<{
46
46
  kind: z.ZodEnum<{
47
- pull_request: "pull_request";
47
+ message: "message";
48
48
  commit: "commit";
49
+ pull_request: "pull_request";
49
50
  issue: "issue";
50
- message: "message";
51
51
  }>;
52
52
  ref: z.ZodString;
53
53
  contentHash: z.ZodString;
@@ -27,6 +27,8 @@ import { compileVerifiedEvidenceMap, EvidenceExecutionSchema, EvidenceInterventi
27
27
  import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
28
28
  import { buildDeliveryEnvelope, DELIVERY_PROFILE_POLICY_VERSION, DELIVERY_PROFILES, } from "../core/delivery.js";
29
29
  import { CHANGE_IDENTITY_ALGORITHM, CHANGE_IDENTITY_SCHEMA_VERSION, deriveChangeIdentity, } from "../core/changeIdentity.js";
30
+ import { deriveChangeProof } from "../core/changeProof.js";
31
+ import { ChangeProofSchema } from "../core/changeProofContract.js";
30
32
  import { PROJECT_DNA_CATEGORIES, PROJECT_DNA_MATCH_SCHEMA_VERSION, PROJECT_DNA_SCHEMA_VERSION, discoverProjectDna, evaluateProjectDnaMatch, } from "../core/projectDna.js";
31
33
  import { PROJECT_DNA_DELTA_SCHEMA_VERSION, diffProjectDna } from "../core/projectDnaDelta.js";
32
34
  import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
@@ -905,6 +907,32 @@ export function buildServerWithRootControl(initialRoot) {
905
907
  return err(error.message);
906
908
  }
907
909
  });
910
+ // -- hunch_change_proof (exact-revision semantic evidence) ----------------
911
+ server.registerTool("hunch_change_proof", {
912
+ title: "Derive a sealed semantic proof for an exact change",
913
+ description: "Bind an exact committed Git transition to its change identity, Project DNA, base/result semantic graphs, current decisions and constraints, blast radius, conformance, guard verdict, and explicit gaps. Read-only and deterministic; grants no execution, CI, deployment, merge, ranking, promotion, or policy authority.",
914
+ inputSchema: {
915
+ base_ref: z.string().min(1).max(1_024).describe("Base commit or ref for the exact tree transition."),
916
+ result_ref: z.string().min(1).max(1_024).optional().describe("Result commit or ref (default HEAD)."),
917
+ public_only: z.boolean().optional().describe("Exclude the configured private-memory overlay. Required before publishing a proof."),
918
+ cwd: cwdHintField,
919
+ },
920
+ outputSchema: ChangeProofSchema,
921
+ }, async ({ base_ref, result_ref, public_only }) => {
922
+ try {
923
+ const proof = ChangeProofSchema.parse(deriveChangeProof(root, store, base_ref, result_ref ?? "HEAD", { publicOnly: public_only }));
924
+ return {
925
+ content: [{
926
+ type: "text",
927
+ text: `${proof.proof_id} — ${proof.verdict.toUpperCase()}; ${proof.changed_file_count} exact file delta(s), ${proof.blast_radius_count} dependent path(s), ${proof.omissions.length + proof.unknowns.length} explicit gap(s); sealed ${proof.content_hash}. Evidence only; no execution or merge authority.`,
928
+ }],
929
+ structuredContent: proof,
930
+ };
931
+ }
932
+ catch (error) {
933
+ return err(error.message);
934
+ }
935
+ });
908
936
  // -- hunch_project_dna ----------------------------------------------------
909
937
  server.registerTool("hunch_project_dna", {
910
938
  title: "Inspect this repository's evidence-backed Project DNA",
@@ -596,7 +596,7 @@ export async function probeOllamaNumCtx(baseUrl, model) {
596
596
  return null;
597
597
  if (/^num_ctx\s+\d+/m.test(body.parameters))
598
598
  return null; // already configured — nothing to warn about
599
- return "⚠ This Ollama model does not pin num_ctx; its effective context depends on server/VRAM defaults. For stable large-diff synthesis, see https://hunch-pi.vercel.app/cookbook and pin num_ctx via a custom Modelfile.";
599
+ return "⚠ This Ollama model does not pin num_ctx; its effective context depends on server/VRAM defaults. For stable large-diff synthesis, see https://www.hunchmemory.com/cookbook and pin num_ctx via a custom Modelfile.";
600
600
  }
601
601
  catch {
602
602
  return null; // not Ollama, unreachable, or an unexpected response shape — advisory only, never throw
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.22.3",
3
+ "version": "1.23.2",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
7
7
  "description": "Engineering memory and a deterministic Change Gate for AI-assisted codebases: decisions, rejected approaches, constraints, and bug lineage become portable context and opt-in enforcement for every MCP assistant.",
8
- "homepage": "https://hunch-pi.vercel.app",
8
+ "homepage": "https://www.hunchmemory.com",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/davesheffer/hunch.git"
@@ -18,6 +18,10 @@
18
18
  "hunch": "dist/cli/index.js"
19
19
  },
20
20
  "exports": {
21
+ "./change-proof": {
22
+ "types": "./dist/changeProof.d.ts",
23
+ "default": "./dist/changeProof.js"
24
+ },
21
25
  "./project-dna": {
22
26
  "types": "./dist/projectDna.d.ts",
23
27
  "default": "./dist/projectDna.js"
@@ -27,8 +31,11 @@
27
31
  },
28
32
  "files": [
29
33
  "dist/**/*.js",
34
+ "dist/changeProof.d.ts",
35
+ "dist/core/changeProofContract.d.ts",
30
36
  "dist/projectDna.d.ts",
31
37
  "dist/core/projectDna*.d.ts",
38
+ "contracts/change-proof/*.json",
32
39
  "server.json",
33
40
  "bench/constitution-exp03-v1.json",
34
41
  "tooling/competitive-watch.mjs",
@@ -76,6 +83,7 @@
76
83
  "rehearse:constitution": "npm run build && node tooling/constitution-clean-rehearsal.mjs",
77
84
  "gate:release": "node tooling/release-gate.mjs",
78
85
  "site:proof": "npm run build && node tooling/generate-public-proof.mjs",
86
+ "site:sitemap": "node tooling/generate-sitemap.mjs",
79
87
  "bench:md1": "node tooling/md1-benchmark.mjs",
80
88
  "research:competitors": "node tooling/competitive-watch.mjs",
81
89
  "outreach": "node tooling/outreach-pipeline.mjs",
package/server.json CHANGED
@@ -6,14 +6,14 @@
6
6
  "url": "https://github.com/davesheffer/hunch",
7
7
  "source": "github"
8
8
  },
9
- "websiteUrl": "https://hunch-pi.vercel.app",
10
- "version": "1.22.3",
9
+ "websiteUrl": "https://www.hunchmemory.com",
10
+ "version": "1.23.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@davesheffer/hunch",
16
- "version": "1.22.3",
16
+ "version": "1.23.2",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {