@davesheffer/hunch 1.20.2 → 1.21.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.
package/README.md CHANGED
@@ -58,6 +58,9 @@ existing configuration.
58
58
  passes, needs attention, or should be blocked.
59
59
  - **Past bugs stay useful** — see which old incident a piece of code fixed before accidentally
60
60
  undoing it.
61
+ - **Fresh context without lost history** — decisions whose code anchors are still current rank
62
+ ahead of ones whose files changed after verification; older evidence remains visible and keeps
63
+ its existing authority.
61
64
  - **Understands how code connects** — for TypeScript, JavaScript, Python, Go, PHP, YAML, and Helm, Hunch
62
65
  can see what calls or depends on the code you are about to change. Its memory works with any
63
66
  language.
@@ -171,7 +174,7 @@ repository, separate from the code repository. Hunch does not host it. Create a
171
174
  that every teammate can access, install Hunch on team machines and CI, then have one maintainer run:
172
175
 
173
176
  ```bash
174
- npm i -g @davesheffer/hunch@1.20.2
177
+ npm i -g @davesheffer/hunch@1.21.0
175
178
  hunch shared --repo git@github.com:acme/project-hunch-memory.git
176
179
  git add .gitignore .hunch/team.json
177
180
  git commit -m "chore: connect shared Hunch memory"
@@ -186,7 +189,7 @@ printed by Hunch. Omit `--migrate` for a new setup.
186
189
  After the pointer commit lands, teammates need Hunch installed and Git access to the memory repo:
187
190
 
188
191
  ```bash
189
- npm i -g @davesheffer/hunch@1.20.2
192
+ npm i -g @davesheffer/hunch@1.21.0
190
193
  git pull
191
194
  hunch init
192
195
  hunch doctor
@@ -234,7 +237,7 @@ but stops automatic memory commits and pushes. As a team-coordinated rollback, r
234
237
  commit to stop discovery after teammates pull the revert. Existing machines retain their ignored
235
238
  local overlay until they are deliberately disconnected; do not delete the memory repo as part of a
236
239
  rollback. For this rollout, reinstall the previous published package with
237
- `npm i -g @davesheffer/hunch@1.20.0`; the release receipt resolves and records the verified rollback
240
+ `npm i -g @davesheffer/hunch@1.20.3`; the release receipt resolves and records the verified rollback
238
241
  target from the npm registry instead of trusting Git tags. Pause enforcement first as shown above,
239
242
  and keep every team client on the same release before resuming Matrix policy workflows.
240
243
 
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import { Command } from "commander";
4
+ import { discoverProjectDna, evaluateProjectDnaMatch } from "../core/projectDna.js";
5
+ import { diffProjectDna } from "../core/projectDnaDelta.js";
6
+ import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
7
+ function gitRoot() {
8
+ try {
9
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "pipe"],
12
+ timeout: 5_000,
13
+ }).trim();
14
+ }
15
+ catch {
16
+ throw new Error("Project DNA must run inside a Git repository");
17
+ }
18
+ }
19
+ function printProfile(root, revision, json) {
20
+ const profile = discoverProjectDna(root, revision);
21
+ if (json) {
22
+ process.stdout.write(`${JSON.stringify(profile, null, 2)}\n`);
23
+ return;
24
+ }
25
+ process.stdout.write(`Project DNA ${profile.profile_id}\n`);
26
+ process.stdout.write(`revision: ${profile.repository_revision}\n`);
27
+ process.stdout.write(`history sample: ${profile.history_sample_count}\n`);
28
+ process.stdout.write(`committed convention files: ${profile.source_files.length ? profile.source_files.join(", ") : "none"}\n`);
29
+ if (!profile.traits.length) {
30
+ process.stdout.write("traits: none yet (insufficient evidence)\n");
31
+ return;
32
+ }
33
+ for (const trait of profile.traits) {
34
+ process.stdout.write(`- [${trait.category}] ${trait.claim} (${trait.confidence.toFixed(2)}; ${trait.id})\n`);
35
+ }
36
+ }
37
+ const program = new Command();
38
+ program
39
+ .name("hunch-dna")
40
+ .description("Derive and inspect Hunch Project DNA from exact committed repository evidence.")
41
+ .option("--revision <ref>", "Git revision to inspect", "HEAD")
42
+ .option("--json", "emit the sealed machine-readable profile")
43
+ .action((options) => {
44
+ printProfile(gitRoot(), options.revision, !!options.json);
45
+ });
46
+ program
47
+ .command("match")
48
+ .description("Score a commit/PR/issue/message against deterministic applicable DNA traits.")
49
+ .requiredOption("--kind <kind>", "commit | pull_request | issue | message")
50
+ .requiredOption("--title <title>", "artifact title/subject")
51
+ .option("--body <body>", "artifact body")
52
+ .option("--revision <ref>", "Git revision whose DNA applies", "HEAD")
53
+ .option("--json", "emit the sealed match contract")
54
+ .action((options) => {
55
+ if (!(new Set(["commit", "pull_request", "issue", "message"])).has(options.kind)) {
56
+ throw new Error("--kind must be commit, pull_request, issue, or message");
57
+ }
58
+ const artifact = {
59
+ kind: options.kind,
60
+ title: options.title,
61
+ ...(options.body !== undefined ? { body: options.body } : {}),
62
+ };
63
+ const globals = program.opts();
64
+ const revision = options.revision ?? globals.revision ?? "HEAD";
65
+ const json = options.json ?? globals.json ?? false;
66
+ const profile = discoverProjectDna(gitRoot(), revision);
67
+ const match = evaluateProjectDnaMatch(profile, artifact);
68
+ if (json)
69
+ process.stdout.write(`${JSON.stringify(match, null, 2)}\n`);
70
+ else {
71
+ process.stdout.write(`Project DNA match: ${match.score === null ? "n/a" : `${match.score}%`} (${match.applicable_checks} applicable checks)\n`);
72
+ for (const check of match.checks.filter((item) => item.applicable)) {
73
+ process.stdout.write(`- ${check.passed ? "PASS" : "FAIL"} ${check.key}: ${check.detail}\n`);
74
+ }
75
+ }
76
+ });
77
+ program
78
+ .command("context")
79
+ .description("Render the bounded advisory DNA supplement intended for Hunch delivery/context assembly.")
80
+ .option("--revision <ref>", "Git revision to inspect", "HEAD")
81
+ .option("--traits <count>", "maximum DNA traits in the orientation slice", "8")
82
+ .option("--json", "emit supplement JSON")
83
+ .action((options) => {
84
+ const globals = program.opts();
85
+ const revision = options.revision ?? globals.revision ?? "HEAD";
86
+ const json = options.json ?? globals.json ?? false;
87
+ const cap = Number(options.traits);
88
+ const supplement = projectDnaDeliverySupplement(discoverProjectDna(gitRoot(), revision), cap);
89
+ if (json)
90
+ process.stdout.write(`${JSON.stringify(supplement, null, 2)}\n`);
91
+ else
92
+ process.stdout.write(`${supplement?.text ?? "No evidence-backed Project DNA traits yet."}\n`);
93
+ });
94
+ program
95
+ .command("diff")
96
+ .description("Compare DNA at two exact revisions; reports observation drift without inferring causality.")
97
+ .argument("<from>", "older Git revision")
98
+ .argument("<to>", "newer Git revision")
99
+ .option("--json", "emit the sealed delta contract")
100
+ .action((from, to, options) => {
101
+ const root = gitRoot();
102
+ const delta = diffProjectDna(discoverProjectDna(root, from), discoverProjectDna(root, to));
103
+ const json = options.json ?? program.opts().json ?? false;
104
+ if (json)
105
+ process.stdout.write(`${JSON.stringify(delta, null, 2)}\n`);
106
+ else {
107
+ process.stdout.write(`Project DNA drift ${delta.delta_id}: ${delta.changed ? `${delta.changes.length} change(s)` : "no observed change"}\n`);
108
+ for (const change of delta.changes)
109
+ process.stdout.write(`- ${change.kind}: ${change.key}\n`);
110
+ }
111
+ });
112
+ program.parseAsync().catch((error) => {
113
+ process.stderr.write(`hunch-dna: ${error instanceof Error ? error.message : String(error)}\n`);
114
+ process.exitCode = 1;
115
+ });
116
+ //# sourceMappingURL=dna.js.map
package/dist/cli/index.js CHANGED
@@ -57,6 +57,9 @@ import { compileVerifiedEvidenceMap, formatVerifiedEvidenceMap } from "../core/e
57
57
  import { collectCorrectionStageSources } from "../extractors/correctionSources.js";
58
58
  import { buildDeliveryEnvelope, DELIVERY_PROFILES } from "../core/delivery.js";
59
59
  import { deriveChangeIdentity } from "../core/changeIdentity.js";
60
+ import { discoverProjectDna, evaluateProjectDnaMatch } from "../core/projectDna.js";
61
+ import { diffProjectDna } from "../core/projectDnaDelta.js";
62
+ import { projectDnaDeliverySupplement } from "../core/projectDnaDelivery.js";
60
63
  import { readConfig, writeConfig, FIRMNESS_LEVELS, isFirmness } from "../core/config.js";
61
64
  import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpolicy.js";
62
65
  import { isHumanConfirmed } from "../core/strictgate.js";
@@ -1292,7 +1295,7 @@ program
1292
1295
  const emb = await selectEmbedder();
1293
1296
  if (!store.semanticReady(emb)) {
1294
1297
  console.log("· semantic search isn't enabled yet — run `hunch embed` (using keyword search for now).\n");
1295
- hits = store.search(q, 12);
1298
+ hits = store.rankedSearch(q, 12);
1296
1299
  }
1297
1300
  else {
1298
1301
  hits = await store.hybridSearch(q, 12, { embedder: emb });
@@ -1300,7 +1303,7 @@ program
1300
1303
  }
1301
1304
  }
1302
1305
  else {
1303
- hits = store.search(q, 12);
1306
+ hits = store.rankedSearch(q, 12);
1304
1307
  }
1305
1308
  if (!hits.length) {
1306
1309
  console.log(`No matches for "${q}".`);
@@ -2903,6 +2906,114 @@ landscapeCmd
2903
2906
  console.log(` hunch landscape adopt --ref ${discovery.sourceRevision} --expected ${discovery.discoveryHash} --all --reviewed-by <you>${discovery.issues.length ? " --acknowledge-issues" : ""}`);
2904
2907
  console.log("Or pass --candidate <hash...> to adopt an explicit subset; relationships require both endpoint resources.");
2905
2908
  });
2909
+ // ---- dna (read-only, exact-revision repository convention profile) -------
2910
+ const dnaCmd = program
2911
+ .command("dna")
2912
+ .description("Inspect evidence-backed Project DNA and evaluate repository-native artifacts without writing graph authority.");
2913
+ dnaCmd
2914
+ .command("inspect")
2915
+ .description("Derive a deterministic Project DNA profile from one exact Git revision. This command never writes memory.")
2916
+ .option("--ref <ref>", "Git commit/ref to inspect", "HEAD")
2917
+ .option("--json", "emit the complete machine-readable profile")
2918
+ .action((opts) => {
2919
+ try {
2920
+ const profile = discoverProjectDna(findRoot(), opts.ref);
2921
+ if (opts.json) {
2922
+ console.log(JSON.stringify(profile, null, 2));
2923
+ return;
2924
+ }
2925
+ console.log(`Project DNA at ${profile.repository_revision}`);
2926
+ console.log(`Profile: ${profile.profile_id} (${profile.content_hash})`);
2927
+ console.log(`Evidence: ${profile.history_sample_count} commit subject(s), ${profile.source_files.length} convention file(s)`);
2928
+ if (profile.source_files.length)
2929
+ console.log(`Sources: ${profile.source_files.join(", ")}`);
2930
+ console.log(`\nTRAITS (${profile.traits.length})`);
2931
+ for (const trait of profile.traits) {
2932
+ console.log(` ${trait.id} [${trait.category}/${trait.confidence.toFixed(3)}] ${trait.claim}`);
2933
+ console.log(` ${trait.evidence.map((evidence) => `${evidence.ref} @ ${evidence.revision.slice(0, 12)} (${evidence.content_hash})`).join("; ")}`);
2934
+ }
2935
+ console.log("\nAdvisory observation only; no trait was adopted into graph authority.");
2936
+ }
2937
+ catch (error) {
2938
+ fail(error instanceof Error ? error.message : String(error));
2939
+ }
2940
+ });
2941
+ dnaCmd
2942
+ .command("match")
2943
+ .description("Explainably score a commit, PR, issue, or message against deterministic checks in an exact-revision DNA profile.")
2944
+ .requiredOption("--kind <kind>", "artifact kind: commit, pull_request, issue, or message")
2945
+ .requiredOption("--title <title>", "artifact title or commit subject")
2946
+ .option("--body <body>", "artifact body (for example a PR description)")
2947
+ .option("--ref <ref>", "Git commit/ref whose DNA should be used", "HEAD")
2948
+ .option("--json", "emit the complete machine-readable match envelope")
2949
+ .action((opts) => {
2950
+ try {
2951
+ if (!["commit", "pull_request", "issue", "message"].includes(opts.kind)) {
2952
+ return fail("--kind must be commit, pull_request, issue, or message");
2953
+ }
2954
+ const profile = discoverProjectDna(findRoot(), opts.ref);
2955
+ const match = evaluateProjectDnaMatch(profile, {
2956
+ kind: opts.kind,
2957
+ title: opts.title,
2958
+ body: opts.body,
2959
+ });
2960
+ if (opts.json) {
2961
+ console.log(JSON.stringify(match, null, 2));
2962
+ return;
2963
+ }
2964
+ console.log(`Project Match: ${match.score === null ? "not enough deterministic checks" : `${match.score.toFixed(1)}/100`}`);
2965
+ console.log(`Profile: ${match.profile_id} @ ${match.repository_revision}`);
2966
+ for (const check of match.checks.filter((candidate) => candidate.applicable)) {
2967
+ console.log(` ${check.passed ? "PASS" : "FAIL"} ${check.key} — ${check.detail}`);
2968
+ }
2969
+ console.log("Advisory only; the score does not grant or change enforcement authority.");
2970
+ }
2971
+ catch (error) {
2972
+ fail(error instanceof Error ? error.message : String(error));
2973
+ }
2974
+ });
2975
+ dnaCmd
2976
+ .command("context")
2977
+ .description("Render the bounded advisory Project DNA supplement used by normal Hunch context delivery.")
2978
+ .option("--ref <ref>", "Git commit/ref to inspect", "HEAD")
2979
+ .option("--traits <count>", "maximum traits in the orientation slice", "8")
2980
+ .option("--json", "emit the complete machine-readable supplement")
2981
+ .action((opts) => {
2982
+ try {
2983
+ const traitCap = Number(opts.traits);
2984
+ const supplement = projectDnaDeliverySupplement(discoverProjectDna(findRoot(), opts.ref), traitCap);
2985
+ if (opts.json)
2986
+ console.log(JSON.stringify(supplement, null, 2));
2987
+ else
2988
+ console.log(supplement?.text ?? "No evidence-backed Project DNA traits yet.");
2989
+ }
2990
+ catch (error) {
2991
+ fail(error instanceof Error ? error.message : String(error));
2992
+ }
2993
+ });
2994
+ dnaCmd
2995
+ .command("diff")
2996
+ .description("Compare two exact-revision DNA profiles without rewriting either snapshot.")
2997
+ .argument("<from>", "older Git revision")
2998
+ .argument("<to>", "newer Git revision")
2999
+ .option("--json", "emit the complete sealed delta")
3000
+ .action((from, to, opts) => {
3001
+ try {
3002
+ const root = findRoot();
3003
+ const delta = diffProjectDna(discoverProjectDna(root, from), discoverProjectDna(root, to));
3004
+ if (opts.json) {
3005
+ console.log(JSON.stringify(delta, null, 2));
3006
+ return;
3007
+ }
3008
+ console.log(`Project DNA drift ${delta.delta_id}: ${delta.changed ? `${delta.changes.length} change(s)` : "no observed change"}`);
3009
+ for (const change of delta.changes)
3010
+ console.log(` ${change.kind} ${change.key}`);
3011
+ console.log("Observation only; no Project DNA profile or graph authority was rewritten.");
3012
+ }
3013
+ catch (error) {
3014
+ fail(error instanceof Error ? error.message : String(error));
3015
+ }
3016
+ });
2906
3017
  landscapeCmd
2907
3018
  .command("adopt")
2908
3019
  .description("Human-confirm a reviewed candidate set and write only those exact records through Hunch's normal graph boundary.")
@@ -3904,7 +4015,7 @@ program
3904
4015
  !ctx.landscape?.resources.length &&
3905
4016
  !ctx.landscape?.relationships.length;
3906
4017
  if (empty && !asOf) {
3907
- const hits = store.search(target, 8);
4018
+ const hits = store.rankedSearch(target, 8);
3908
4019
  if (hits.length) {
3909
4020
  console.log(`No file/symbol resolves for "${target}" — closest graph matches instead:\n`);
3910
4021
  for (const h of hits)