@davesheffer/hunch 1.20.3 → 1.21.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.
- package/README.md +3 -3
- package/dist/cli/dna.js +116 -0
- package/dist/cli/index.js +112 -1
- package/dist/cli/invocation.js +16 -3
- package/dist/core/projectDna.js +478 -0
- package/dist/core/projectDnaDelivery.js +54 -0
- package/dist/core/projectDnaDelta.js +159 -0
- package/dist/integrations/scaffold.js +18 -7
- package/dist/mcp/server.js +161 -8
- package/dist/projectDna.js +10 -0
- package/package.json +9 -1
- package/server.json +2 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { compareCodeUnits } from "./canonicalOrder.js";
|
|
3
|
+
import { assertProjectDnaProfile } from "./projectDna.js";
|
|
4
|
+
export const PROJECT_DNA_DELTA_SCHEMA_VERSION = "hunch.project-dna-delta/1";
|
|
5
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
6
|
+
const DELTA_ID = /^pdnad_[a-f0-9]{24}$/;
|
|
7
|
+
function canonical(value) {
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
return `{${Object.entries(value)
|
|
12
|
+
.filter(([, child]) => child !== undefined)
|
|
13
|
+
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
14
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`)
|
|
15
|
+
.join(",")}}`;
|
|
16
|
+
}
|
|
17
|
+
return JSON.stringify(value) ?? "null";
|
|
18
|
+
}
|
|
19
|
+
function sha256(value) {
|
|
20
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
21
|
+
}
|
|
22
|
+
function evidenceSeal(trait) {
|
|
23
|
+
return sha256(canonical(trait.evidence));
|
|
24
|
+
}
|
|
25
|
+
function mapTraits(profile) {
|
|
26
|
+
return new Map(profile.traits.map((trait) => [trait.key, trait]));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compare two already-sealed profiles without inferring causality.
|
|
30
|
+
*
|
|
31
|
+
* A delta says only that observed DNA changed between exact revisions. It does
|
|
32
|
+
* not promote the new trait, explain why the change happened, or grant policy.
|
|
33
|
+
*/
|
|
34
|
+
export function diffProjectDna(fromValue, toValue) {
|
|
35
|
+
assertProjectDnaProfile(fromValue);
|
|
36
|
+
assertProjectDnaProfile(toValue);
|
|
37
|
+
const from = fromValue;
|
|
38
|
+
const to = toValue;
|
|
39
|
+
if (from.repository_id !== to.repository_id) {
|
|
40
|
+
throw new Error("project DNA profiles belong to different repositories");
|
|
41
|
+
}
|
|
42
|
+
const before = mapTraits(from);
|
|
43
|
+
const after = mapTraits(to);
|
|
44
|
+
const keys = [...new Set([...before.keys(), ...after.keys()])].sort(compareCodeUnits);
|
|
45
|
+
const changes = [];
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const left = before.get(key);
|
|
48
|
+
const right = after.get(key);
|
|
49
|
+
if (!left && right) {
|
|
50
|
+
changes.push({
|
|
51
|
+
key,
|
|
52
|
+
kind: "added",
|
|
53
|
+
before_trait_id: null,
|
|
54
|
+
after_trait_id: right.id,
|
|
55
|
+
before_confidence: null,
|
|
56
|
+
after_confidence: right.confidence,
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (left && !right) {
|
|
61
|
+
changes.push({
|
|
62
|
+
key,
|
|
63
|
+
kind: "removed",
|
|
64
|
+
before_trait_id: left.id,
|
|
65
|
+
after_trait_id: null,
|
|
66
|
+
before_confidence: left.confidence,
|
|
67
|
+
after_confidence: null,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!left || !right)
|
|
72
|
+
continue;
|
|
73
|
+
if (left.id !== right.id || evidenceSeal(left) !== evidenceSeal(right)) {
|
|
74
|
+
changes.push({
|
|
75
|
+
key,
|
|
76
|
+
kind: "evidence_changed",
|
|
77
|
+
before_trait_id: left.id,
|
|
78
|
+
after_trait_id: right.id,
|
|
79
|
+
before_confidence: left.confidence,
|
|
80
|
+
after_confidence: right.confidence,
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (left.confidence !== right.confidence) {
|
|
85
|
+
changes.push({
|
|
86
|
+
key,
|
|
87
|
+
kind: "confidence_changed",
|
|
88
|
+
before_trait_id: left.id,
|
|
89
|
+
after_trait_id: right.id,
|
|
90
|
+
before_confidence: left.confidence,
|
|
91
|
+
after_confidence: right.confidence,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const unsigned = {
|
|
96
|
+
schema: PROJECT_DNA_DELTA_SCHEMA_VERSION,
|
|
97
|
+
repository_id: from.repository_id,
|
|
98
|
+
from_profile_id: from.profile_id,
|
|
99
|
+
to_profile_id: to.profile_id,
|
|
100
|
+
from_revision: from.repository_revision,
|
|
101
|
+
to_revision: to.repository_revision,
|
|
102
|
+
changes,
|
|
103
|
+
changed: changes.length > 0,
|
|
104
|
+
};
|
|
105
|
+
const deltaId = `pdnad_${sha256(canonical(unsigned)).slice("sha256:".length, "sha256:".length + 24)}`;
|
|
106
|
+
const sealed = { ...unsigned, delta_id: deltaId };
|
|
107
|
+
const delta = { ...sealed, content_hash: sha256(canonical(sealed)) };
|
|
108
|
+
assertProjectDnaDelta(delta);
|
|
109
|
+
return delta;
|
|
110
|
+
}
|
|
111
|
+
export function assertProjectDnaDelta(value) {
|
|
112
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
113
|
+
throw new Error("project DNA delta is invalid");
|
|
114
|
+
const delta = value;
|
|
115
|
+
const expectedFields = [
|
|
116
|
+
"schema", "delta_id", "repository_id", "from_profile_id", "to_profile_id", "from_revision", "to_revision", "changes", "changed", "content_hash",
|
|
117
|
+
].sort(compareCodeUnits);
|
|
118
|
+
if (Object.keys(value).sort(compareCodeUnits).join("\0") !== expectedFields.join("\0")
|
|
119
|
+
|| delta.schema !== PROJECT_DNA_DELTA_SCHEMA_VERSION || !DELTA_ID.test(delta.delta_id)
|
|
120
|
+
|| !/^pdnar_[a-f0-9]{24}$/.test(delta.repository_id)
|
|
121
|
+
|| !/^pdna_[a-f0-9]{24}$/.test(delta.from_profile_id) || !/^pdna_[a-f0-9]{24}$/.test(delta.to_profile_id)
|
|
122
|
+
|| !/^[a-f0-9]{40,64}$/.test(delta.from_revision) || !/^[a-f0-9]{40,64}$/.test(delta.to_revision)
|
|
123
|
+
|| !Array.isArray(delta.changes) || delta.changes.length > 128 || delta.changed !== (delta.changes.length > 0)
|
|
124
|
+
|| !SHA256.test(delta.content_hash)) {
|
|
125
|
+
throw new Error("project DNA delta fields are invalid");
|
|
126
|
+
}
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
for (const change of delta.changes) {
|
|
129
|
+
if (!change || typeof change !== "object" || Array.isArray(change))
|
|
130
|
+
throw new Error("project DNA trait change is invalid");
|
|
131
|
+
const fields = ["key", "kind", "before_trait_id", "after_trait_id", "before_confidence", "after_confidence"].sort(compareCodeUnits);
|
|
132
|
+
if (Object.keys(change).sort(compareCodeUnits).join("\0") !== fields.join("\0")
|
|
133
|
+
|| !/^[a-z][a-z0-9_.-]{2,100}$/.test(change.key) || seen.has(change.key)
|
|
134
|
+
|| !["added", "removed", "evidence_changed", "confidence_changed"].includes(change.kind)
|
|
135
|
+
|| (change.before_trait_id !== null && !/^pdnat_[a-f0-9]{20}$/.test(change.before_trait_id))
|
|
136
|
+
|| (change.after_trait_id !== null && !/^pdnat_[a-f0-9]{20}$/.test(change.after_trait_id))
|
|
137
|
+
|| (change.before_confidence !== null && (!Number.isFinite(change.before_confidence) || change.before_confidence < 0 || change.before_confidence > 1))
|
|
138
|
+
|| (change.after_confidence !== null && (!Number.isFinite(change.after_confidence) || change.after_confidence < 0 || change.after_confidence > 1))) {
|
|
139
|
+
throw new Error("project DNA trait change fields are invalid");
|
|
140
|
+
}
|
|
141
|
+
if ((change.kind === "added" && (change.before_trait_id !== null || change.before_confidence !== null
|
|
142
|
+
|| change.after_trait_id === null || change.after_confidence === null))
|
|
143
|
+
|| (change.kind === "removed" && (change.before_trait_id === null || change.before_confidence === null
|
|
144
|
+
|| change.after_trait_id !== null || change.after_confidence !== null))
|
|
145
|
+
|| ((change.kind === "evidence_changed" || change.kind === "confidence_changed")
|
|
146
|
+
&& (change.before_trait_id === null || change.before_confidence === null
|
|
147
|
+
|| change.after_trait_id === null || change.after_confidence === null))) {
|
|
148
|
+
throw new Error("project DNA trait change transition is invalid");
|
|
149
|
+
}
|
|
150
|
+
seen.add(change.key);
|
|
151
|
+
}
|
|
152
|
+
const { content_hash: _contentHash, delta_id: _deltaId, ...base } = delta;
|
|
153
|
+
const expectedId = `pdnad_${sha256(canonical(base)).slice("sha256:".length, "sha256:".length + 24)}`;
|
|
154
|
+
const sealed = { ...base, delta_id: delta.delta_id };
|
|
155
|
+
if (delta.delta_id !== expectedId || delta.content_hash !== sha256(canonical(sealed))) {
|
|
156
|
+
throw new Error("project DNA delta seal is invalid");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=projectDnaDelta.js.map
|
|
@@ -101,14 +101,25 @@ Reconcile decision-grounding drift for **$ARGUMENTS** (or the whole repo).
|
|
|
101
101
|
3. Only if I explicitly say "the DECISION is stale, not the doc" (Heal B): run /capture to record a superseding decision, then return to step 2 — the prose re-derives from the new decision as a separate confirm.
|
|
102
102
|
4. Report: healed (Heal A), superseded (Heal B), skipped. Never touch the graph except via an explicit Heal B capture.
|
|
103
103
|
`;
|
|
104
|
-
/** A settings.json hook entry is Hunch's if any of its commands
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
104
|
+
/** A settings.json hook entry is Hunch's if any of its commands is either the
|
|
105
|
+
* native/source CLI entry (`…/index.js hook`) or the exact published-package
|
|
106
|
+
* launcher written by older Hunch versions (`npx --package=…@davesheffer/hunch…
|
|
107
|
+
* hunch hook`). Matching both generations makes an upgrade idempotent instead
|
|
108
|
+
* of leaving the portable old hook alongside the new native invocation. The
|
|
109
|
+
* source form still requires `index` to be a full path segment, and the npx form
|
|
110
|
+
* requires both the scoped package and the `hunch hook` tail, so foreign hooks
|
|
111
|
+
* are preserved. */
|
|
110
112
|
function isHunchHook(entry) {
|
|
111
|
-
return !!entry.hooks?.some((h) =>
|
|
113
|
+
return !!entry.hooks?.some((h) => {
|
|
114
|
+
if (typeof h.command !== "string")
|
|
115
|
+
return false;
|
|
116
|
+
const command = h.command;
|
|
117
|
+
const nativeOrSource = /[\\/]index\.(js|ts)"?\s+hook\s*$/.test(command);
|
|
118
|
+
const publishedNpx = /^\s*"?npx(?:\.cmd)?"?\s+/i.test(command)
|
|
119
|
+
&& /--package=(?:hunch-exact@npm:)?@davesheffer\/hunch(?:@[^"\s]+)?/.test(command)
|
|
120
|
+
&& /\s"?hunch"?\s+"?hook"?\s*$/.test(command);
|
|
121
|
+
return nativeOrSource || publishedNpx;
|
|
122
|
+
});
|
|
112
123
|
}
|
|
113
124
|
/**
|
|
114
125
|
* Install the Claude Code AGENT hooks into `.claude/settings.json` so the agent
|
package/dist/mcp/server.js
CHANGED
|
@@ -27,6 +27,9 @@ 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 { PROJECT_DNA_CATEGORIES, PROJECT_DNA_MATCH_SCHEMA_VERSION, PROJECT_DNA_SCHEMA_VERSION, discoverProjectDna, evaluateProjectDnaMatch, } from "../core/projectDna.js";
|
|
31
|
+
import { PROJECT_DNA_DELTA_SCHEMA_VERSION, diffProjectDna } from "../core/projectDnaDelta.js";
|
|
32
|
+
import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
|
|
30
33
|
import { armExecutionObligations, loadPipelineState, savePipelineState } from "../core/pipeline.js";
|
|
31
34
|
import { recordServed } from "../core/served.js";
|
|
32
35
|
import { EdgeSchema, ResourceSchema } from "../core/types.js";
|
|
@@ -250,6 +253,73 @@ const CHANGE_IDENTITY_OUTPUT_SCHEMA = z.object({
|
|
|
250
253
|
paths_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
251
254
|
content_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
252
255
|
});
|
|
256
|
+
const PROJECT_DNA_EVIDENCE_SCHEMA = z.object({
|
|
257
|
+
kind: z.enum(["git-history", "committed-file"]),
|
|
258
|
+
ref: z.string(),
|
|
259
|
+
revision: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
260
|
+
content_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
261
|
+
sample_count: z.number().int().positive(),
|
|
262
|
+
provenance: z.literal("committed-repository"),
|
|
263
|
+
visibility: z.literal("repository"),
|
|
264
|
+
});
|
|
265
|
+
const PROJECT_DNA_PROFILE_OUTPUT_SCHEMA = z.object({
|
|
266
|
+
schema: z.literal(PROJECT_DNA_SCHEMA_VERSION),
|
|
267
|
+
profile_id: z.string().regex(/^pdna_[a-f0-9]{24}$/),
|
|
268
|
+
repository_id: z.string().regex(/^pdnar_[a-f0-9]{24}$/),
|
|
269
|
+
repository_revision: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
270
|
+
history_sample_count: z.number().int().nonnegative(),
|
|
271
|
+
source_files: z.array(z.string()),
|
|
272
|
+
traits: z.array(z.object({
|
|
273
|
+
id: z.string().regex(/^pdnat_[a-f0-9]{20}$/),
|
|
274
|
+
category: z.enum(PROJECT_DNA_CATEGORIES),
|
|
275
|
+
key: z.string(),
|
|
276
|
+
claim: z.string(),
|
|
277
|
+
confidence: z.number().min(0).max(1),
|
|
278
|
+
observation_state: z.literal("observed"),
|
|
279
|
+
freshness: z.literal("current"),
|
|
280
|
+
contradiction: z.literal("none"),
|
|
281
|
+
evidence: z.array(PROJECT_DNA_EVIDENCE_SCHEMA),
|
|
282
|
+
})),
|
|
283
|
+
content_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
284
|
+
});
|
|
285
|
+
const PROJECT_DNA_MATCH_OUTPUT_SCHEMA = z.object({
|
|
286
|
+
schema: z.literal(PROJECT_DNA_MATCH_SCHEMA_VERSION),
|
|
287
|
+
match_id: z.string().regex(/^pdnam_[a-f0-9]{24}$/),
|
|
288
|
+
profile_id: z.string().regex(/^pdna_[a-f0-9]{24}$/),
|
|
289
|
+
repository_id: z.string().regex(/^pdnar_[a-f0-9]{24}$/),
|
|
290
|
+
repository_revision: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
291
|
+
artifact_kind: z.enum(["commit", "pull_request", "issue", "message"]),
|
|
292
|
+
score: z.number().min(0).max(100).nullable(),
|
|
293
|
+
applicable_checks: z.number().int().nonnegative(),
|
|
294
|
+
checks: z.array(z.object({
|
|
295
|
+
trait_id: z.string().regex(/^pdnat_[a-f0-9]{20}$/),
|
|
296
|
+
key: z.string(),
|
|
297
|
+
applicable: z.boolean(),
|
|
298
|
+
passed: z.boolean().nullable(),
|
|
299
|
+
weight: z.number().int().positive(),
|
|
300
|
+
detail: z.string(),
|
|
301
|
+
})),
|
|
302
|
+
content_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
303
|
+
});
|
|
304
|
+
const PROJECT_DNA_DELTA_OUTPUT_SCHEMA = z.object({
|
|
305
|
+
schema: z.literal(PROJECT_DNA_DELTA_SCHEMA_VERSION),
|
|
306
|
+
delta_id: z.string().regex(/^pdnad_[a-f0-9]{24}$/),
|
|
307
|
+
repository_id: z.string().regex(/^pdnar_[a-f0-9]{24}$/),
|
|
308
|
+
from_profile_id: z.string().regex(/^pdna_[a-f0-9]{24}$/),
|
|
309
|
+
to_profile_id: z.string().regex(/^pdna_[a-f0-9]{24}$/),
|
|
310
|
+
from_revision: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
311
|
+
to_revision: z.string().regex(/^[a-f0-9]{40,64}$/),
|
|
312
|
+
changes: z.array(z.object({
|
|
313
|
+
key: z.string(),
|
|
314
|
+
kind: z.enum(["added", "removed", "evidence_changed", "confidence_changed"]),
|
|
315
|
+
before_trait_id: z.string().regex(/^pdnat_[a-f0-9]{20}$/).nullable(),
|
|
316
|
+
after_trait_id: z.string().regex(/^pdnat_[a-f0-9]{20}$/).nullable(),
|
|
317
|
+
before_confidence: z.number().min(0).max(1).nullable(),
|
|
318
|
+
after_confidence: z.number().min(0).max(1).nullable(),
|
|
319
|
+
})),
|
|
320
|
+
changed: z.boolean(),
|
|
321
|
+
content_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
322
|
+
});
|
|
253
323
|
/** Return the same human-readable brief older clients consume plus the exact
|
|
254
324
|
* machine-readable envelope. Receipt recording is deliberately best-effort:
|
|
255
325
|
* recordServed never throws, so telemetry can never cost a delivery. */
|
|
@@ -835,6 +905,76 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
835
905
|
return err(error.message);
|
|
836
906
|
}
|
|
837
907
|
});
|
|
908
|
+
// -- hunch_project_dna ----------------------------------------------------
|
|
909
|
+
server.registerTool("hunch_project_dna", {
|
|
910
|
+
title: "Inspect this repository's evidence-backed Project DNA",
|
|
911
|
+
description: "Derive a bounded, deterministic profile of how this repository communicates, reviews, and builds from one exact committed revision. Read-only: observations remain advisory and are never adopted into graph authority automatically.",
|
|
912
|
+
inputSchema: {
|
|
913
|
+
ref: z.string().min(1).max(1_024).optional().describe("Git commit/ref to inspect (default HEAD)."),
|
|
914
|
+
cwd: cwdHintField,
|
|
915
|
+
},
|
|
916
|
+
outputSchema: PROJECT_DNA_PROFILE_OUTPUT_SCHEMA,
|
|
917
|
+
}, async ({ ref }) => {
|
|
918
|
+
try {
|
|
919
|
+
const profile = PROJECT_DNA_PROFILE_OUTPUT_SCHEMA.parse(discoverProjectDna(root, ref ?? "HEAD"));
|
|
920
|
+
const headline = `${profile.profile_id} at ${profile.repository_revision}: ${profile.traits.length} evidence-backed trait(s) from ${profile.history_sample_count} commit subject(s) and ${profile.source_files.length} convention file(s). Advisory observation only.`;
|
|
921
|
+
return { content: [{ type: "text", text: headline }], structuredContent: profile };
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
return err(error.message);
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
// -- hunch_project_match --------------------------------------------------
|
|
928
|
+
server.registerTool("hunch_project_match", {
|
|
929
|
+
title: "Evaluate whether an artifact matches this repository's Project DNA",
|
|
930
|
+
description: "Explainably score a commit subject, PR, issue, or message using only deterministic checks supported by an exact-revision Project DNA profile. Advisory: never changes policy or enforcement authority.",
|
|
931
|
+
inputSchema: {
|
|
932
|
+
kind: z.enum(["commit", "pull_request", "issue", "message"]),
|
|
933
|
+
title: z.string().min(1).max(1_000),
|
|
934
|
+
body: z.string().max(20_000).optional(),
|
|
935
|
+
ref: z.string().min(1).max(1_024).optional().describe("Git commit/ref whose DNA should be used (default HEAD)."),
|
|
936
|
+
cwd: cwdHintField,
|
|
937
|
+
},
|
|
938
|
+
outputSchema: PROJECT_DNA_MATCH_OUTPUT_SCHEMA,
|
|
939
|
+
}, async ({ kind, title, body, ref }) => {
|
|
940
|
+
try {
|
|
941
|
+
const profile = discoverProjectDna(root, ref ?? "HEAD");
|
|
942
|
+
const match = PROJECT_DNA_MATCH_OUTPUT_SCHEMA.parse(evaluateProjectDnaMatch(profile, { kind, title, body }));
|
|
943
|
+
const failed = match.checks.filter((check) => check.applicable && !check.passed).map((check) => check.key);
|
|
944
|
+
const headline = match.score === null
|
|
945
|
+
? `${match.match_id}: no deterministic DNA checks apply to this artifact.`
|
|
946
|
+
: `${match.match_id}: Project Match ${match.score.toFixed(1)}/100 across ${match.applicable_checks} check(s)${failed.length ? `; mismatches: ${failed.join(", ")}` : ""}. Advisory only.`;
|
|
947
|
+
return { content: [{ type: "text", text: headline }], structuredContent: match };
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
return err(error.message);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
// -- hunch_project_dna_delta ---------------------------------------------
|
|
954
|
+
server.registerTool("hunch_project_dna_delta", {
|
|
955
|
+
title: "Compare Project DNA across two exact repository revisions",
|
|
956
|
+
description: "Return a sealed, explainable delta between two immutable Project DNA profiles. Read-only: reports observation drift and never rewrites history or graph authority.",
|
|
957
|
+
inputSchema: {
|
|
958
|
+
from_ref: z.string().min(1).max(1_024).describe("Older Git commit/ref."),
|
|
959
|
+
to_ref: z.string().min(1).max(1_024).describe("Newer Git commit/ref."),
|
|
960
|
+
cwd: cwdHintField,
|
|
961
|
+
},
|
|
962
|
+
outputSchema: PROJECT_DNA_DELTA_OUTPUT_SCHEMA,
|
|
963
|
+
}, async ({ from_ref, to_ref }) => {
|
|
964
|
+
try {
|
|
965
|
+
const delta = PROJECT_DNA_DELTA_OUTPUT_SCHEMA.parse(diffProjectDna(discoverProjectDna(root, from_ref), discoverProjectDna(root, to_ref)));
|
|
966
|
+
return {
|
|
967
|
+
content: [{
|
|
968
|
+
type: "text",
|
|
969
|
+
text: `${delta.delta_id}: ${delta.changed ? `${delta.changes.length} observed DNA change(s)` : "no observed DNA change"} from ${delta.from_revision} to ${delta.to_revision}. Advisory only.`,
|
|
970
|
+
}],
|
|
971
|
+
structuredContent: delta,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
return err(error.message);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
838
978
|
// -- hunch_context (surgical retrieval) -----------------------------------
|
|
839
979
|
server.registerTool("hunch_context", {
|
|
840
980
|
title: "Assemble the minimal relevant Hunch slice for a task",
|
|
@@ -851,6 +991,15 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
851
991
|
if (as_of && !asOf)
|
|
852
992
|
return err(`Could not resolve as_of "${as_of}" to a commit.`);
|
|
853
993
|
const ctx = store.assembleContext(target, budget_tokens ?? 1500, { asOf });
|
|
994
|
+
let dnaSupplement = null;
|
|
995
|
+
try {
|
|
996
|
+
dnaSupplement = projectDnaDeliverySupplement(discoverProjectDna(root, as_of ?? "HEAD"));
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// Context retrieval must keep its existing graceful behavior when the
|
|
1000
|
+
// Git checkout cannot provide DNA; the dedicated DNA tool reports the
|
|
1001
|
+
// exact derivation error when a caller needs diagnostics.
|
|
1002
|
+
}
|
|
854
1003
|
const options = {
|
|
855
1004
|
root,
|
|
856
1005
|
symbols: store.recs("symbols"),
|
|
@@ -858,6 +1007,7 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
858
1007
|
decisionCorpus: store.recs("decisions"),
|
|
859
1008
|
historical: !!asOf,
|
|
860
1009
|
profile: profile ?? "builder",
|
|
1010
|
+
supplements: dnaSupplement ? [dnaSupplement] : [],
|
|
861
1011
|
};
|
|
862
1012
|
// Task-phrase input ("improve retrieval ranking") resolves no file/symbol and
|
|
863
1013
|
// used to return an empty brief while the graph held the answer — fall back to
|
|
@@ -882,14 +1032,17 @@ export function buildServerWithRootControl(initialRoot) {
|
|
|
882
1032
|
};
|
|
883
1033
|
const envelope = buildDeliveryEnvelope(fallback, {
|
|
884
1034
|
...options,
|
|
885
|
-
supplements:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1035
|
+
supplements: [
|
|
1036
|
+
...(dnaSupplement ? [dnaSupplement] : []),
|
|
1037
|
+
...hits
|
|
1038
|
+
.filter((hit) => !["constraints", "decisions", "bugs", "findings"].includes(hit.kind))
|
|
1039
|
+
.map((hit, index) => ({
|
|
1040
|
+
id: hit.ref,
|
|
1041
|
+
kind: `search-${hit.kind}`,
|
|
1042
|
+
text: `${hit.ref} — ${hit.title}: ${hit.snippet}`,
|
|
1043
|
+
priority: 100 - index,
|
|
1044
|
+
})),
|
|
1045
|
+
],
|
|
893
1046
|
});
|
|
894
1047
|
return deliveredContext(root, target, envelope, extra.sessionId);
|
|
895
1048
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable public programmatic surface for the Project DNA Engine.
|
|
3
|
+
*
|
|
4
|
+
* Keep transport/orchestration consumers on this barrel so internal core file
|
|
5
|
+
* layout can evolve without changing the published contract entry point.
|
|
6
|
+
*/
|
|
7
|
+
export { PROJECT_DNA_CATEGORIES, PROJECT_DNA_MATCH_SCHEMA_VERSION, PROJECT_DNA_SCHEMA_VERSION, assertProjectDnaMatch, assertProjectDnaProfile, discoverProjectDna, evaluateProjectDnaMatch, } from "./core/projectDna.js";
|
|
8
|
+
export { PROJECT_DNA_DELTA_SCHEMA_VERSION, assertProjectDnaDelta, diffProjectDna, } from "./core/projectDnaDelta.js";
|
|
9
|
+
export { PROJECT_DNA_SUPPLEMENT_KIND, projectDnaDeliverySupplement, } from "./core/projectDnaDelivery.js";
|
|
10
|
+
//# sourceMappingURL=projectDna.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"mcpName": "io.github.davesheffer/hunch",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
"bin": {
|
|
18
18
|
"hunch": "dist/cli/index.js"
|
|
19
19
|
},
|
|
20
|
+
"exports": {
|
|
21
|
+
"./project-dna": {
|
|
22
|
+
"types": "./dist/projectDna.d.ts",
|
|
23
|
+
"default": "./dist/projectDna.js"
|
|
24
|
+
},
|
|
25
|
+
"./dist/*": "./dist/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
20
28
|
"files": [
|
|
21
29
|
"dist/**/*.js",
|
|
22
30
|
"server.json",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://hunch-pi.vercel.app",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.21.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.21.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|