@davesheffer/hunch 0.29.0 → 0.30.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/dist/cli/index.js CHANGED
@@ -47,6 +47,7 @@ import { blockingInScope, vetoInScope, proposedEditLines } from "../core/hookpol
47
47
  import { loadGoldenSet, evaluateGraphLift } from "../eval/harness.js";
48
48
  import { computeDrift } from "../core/drift.js";
49
49
  import { compareCandidates } from "../core/compare.js";
50
+ import { checkConformance } from "../core/conformance.js";
50
51
  import { draftTripwires, knownRepoDeps } from "../synthesis/tripwires.js";
51
52
  import { constraintId } from "../core/ids.js";
52
53
  import { readManifest, writeManifest, SCHEMA_VERSION } from "../core/migrate.js";
@@ -598,6 +599,37 @@ program
598
599
  console.log(`✓ captured ${dec} decision(s) + ${con} constraint(s) from inline comments${opts.private ? " [private overlay]" : ""}`);
599
600
  store.close();
600
601
  });
602
+ // ---- conform (intent-conformance: does code still satisfy the recorded why) ----
603
+ program
604
+ .command("conform")
605
+ .description("Intent-conformance: prove the code still SATISFIES each in-force decision's recorded intent (deterministic, over the graph). Surfaces where code drifted from the why — even with no diff in scope.")
606
+ .option("--strict", "exit non-zero if any intent is violated")
607
+ .action((opts) => {
608
+ const { store } = storeFor();
609
+ store.reindex();
610
+ const results = checkConformance(store);
611
+ if (!results.length) {
612
+ console.log("No conformance predicates recorded yet.");
613
+ console.log(dim(" Add a `conformance` predicate to a decision (e.g. { assert: \"calls\", subject: \"pay\", object: \"verifySession\" }) to prove the code honors its intent."));
614
+ store.close();
615
+ return;
616
+ }
617
+ const violations = results.filter((r) => !r.satisfied);
618
+ console.log(`Intent-conformance: ${results.length - violations.length}/${results.length} satisfied\n`);
619
+ for (const r of results) {
620
+ console.log(` ${r.satisfied ? "✅" : "⛔"} ${r.decision} — "${r.title}"`);
621
+ console.log(` ${r.assert} ${r.subject}${r.object ? ` → ${r.object}` : ""}: ${r.detail}`);
622
+ }
623
+ if (violations.length) {
624
+ console.log(`\n⛔ ${violations.length} intent(s) the code no longer satisfies.`);
625
+ if (opts.strict)
626
+ process.exitCode = 1;
627
+ }
628
+ else {
629
+ console.log(`\n✅ the code satisfies every recorded intent.`);
630
+ }
631
+ store.close();
632
+ });
601
633
  // ---- compare (rank N candidate solutions by architectural fit) ------------
602
634
  program
603
635
  .command("compare")
@@ -0,0 +1,57 @@
1
+ function resolveSymbol(store, ref) {
2
+ const syms = store.recs("symbols");
3
+ if (ref.startsWith("sym_"))
4
+ return syms.find((s) => s.id === ref) ?? null;
5
+ if (ref.includes(":")) {
6
+ const [f, n] = ref.split(":");
7
+ return syms.find((s) => s.name === n && (s.file === f || s.file.endsWith("/" + (f ?? "")))) ?? null;
8
+ }
9
+ return syms.find((s) => s.name === ref) ?? null;
10
+ }
11
+ function reaches(store, id, transitive) {
12
+ const set = new Set();
13
+ for (const d of store.getDependencies(id, transitive ? 6 : 1)) {
14
+ if (transitive || d.depth === 1)
15
+ set.add(d.id);
16
+ }
17
+ return set;
18
+ }
19
+ function evalPredicate(store, d, p) {
20
+ const base = { decision: d.id, title: d.title, assert: p.assert, subject: p.subject, object: p.object };
21
+ const subj = resolveSymbol(store, p.subject);
22
+ if (p.assert === "exists") {
23
+ return { ...base, satisfied: !!subj, detail: subj ? `${p.subject} exists (${subj.file})` : `${p.subject} no longer exists in the graph` };
24
+ }
25
+ if (!subj)
26
+ return { ...base, satisfied: false, detail: `subject "${p.subject}" not found in the graph — intent's subject is gone` };
27
+ const wantReach = p.assert === "calls" || p.assert === "imports";
28
+ const obj = p.object ? resolveSymbol(store, p.object) : null;
29
+ if (!obj) {
30
+ // a required target gone ⇒ the link can't hold (violated); a forbidden one trivially holds.
31
+ return { ...base, satisfied: !wantReach, detail: `target "${p.object ?? ""}" not found in the graph` };
32
+ }
33
+ const linked = reaches(store, subj.id, p.transitive).has(obj.id);
34
+ const satisfied = wantReach ? linked : !linked;
35
+ const via = p.transitive ? " (transitively)" : "";
36
+ const detail = satisfied
37
+ ? wantReach
38
+ ? `${subj.name} →${via} ${obj.name} ✓`
39
+ : `${subj.name} does not reach ${obj.name} ✓`
40
+ : wantReach
41
+ ? `${subj.name} no longer reaches${via} ${obj.name} — intent VIOLATED`
42
+ : `${subj.name} now reaches${via} ${obj.name} — intent VIOLATED`;
43
+ return { ...base, satisfied, detail };
44
+ }
45
+ /** Check every in-force decision's conformance predicates against the CURRENT graph.
46
+ * `.satisfied === false` means the code drifted from the recorded intent. Deterministic. */
47
+ export function checkConformance(store) {
48
+ const out = [];
49
+ for (const d of store.recs("decisions")) {
50
+ if (d.status === "superseded" || d.superseded_by)
51
+ continue; // in-force decisions only
52
+ for (const p of d.conformance ?? [])
53
+ out.push(evalPredicate(store, d, p));
54
+ }
55
+ return out;
56
+ }
57
+ //# sourceMappingURL=conformance.js.map
@@ -94,6 +94,18 @@ export const RejectedTripwireSchema = z.object({
94
94
  provenance: ProvenanceSchema,
95
95
  });
96
96
  /** ADR-style decision record, auto-drafted and human-confirmable. */
97
+ /** Intent-conformance predicate (the "inversion": prove the code still SATISFIES a
98
+ * decision's intent, not just that a diff didn't touch a guarded file). Each predicate
99
+ * compiles a decision's intent into a DETERMINISTIC check over the symbol/dependency
100
+ * graph Hunch already builds — no model. "pay must verify the session" becomes
101
+ * { assert: "calls", subject: "pay", object: "verifySession" }; if pay stops calling
102
+ * verifySession the intent is VIOLATED even with no diff in scope. */
103
+ export const ConformancePredicateSchema = z.object({
104
+ assert: z.enum(["calls", "not-calls", "imports", "not-imports", "exists"]),
105
+ subject: z.string().describe("symbol name / id / file:name the intent is about"),
106
+ object: z.string().optional().describe("required (calls/imports) or forbidden (not-*) target"),
107
+ transitive: z.boolean().default(false).describe("allow an indirect path over the dependency graph"),
108
+ });
97
109
  export const DecisionSchema = z.object({
98
110
  id: z.string().describe("dec_*"),
99
111
  title: z.string(),
@@ -117,6 +129,7 @@ export const DecisionSchema = z.object({
117
129
  valid_from: z.string().optional().describe("ISO instant the decision took effect (commit date)"),
118
130
  valid_to: z.string().nullable().default(null).describe("ISO instant it was superseded (null = in force)"),
119
131
  retired: RetiredSignalSchema.default({ symbols: [], deps: [] }),
132
+ conformance: z.array(ConformancePredicateSchema).optional().describe("deterministic intent-conformance checks over the graph"),
120
133
  provenance: ProvenanceSchema,
121
134
  date: z.string(),
122
135
  });
@@ -34,6 +34,7 @@ export function renderHunchSection(store) {
34
34
  lines.push("- `hunch_query(query)` — free-text search across all of Hunch.");
35
35
  lines.push("- `hunch_runbook(task)` — the proven steps for a recurring task (e.g. \"add an MCP tool\", \"cut a release\").");
36
36
  lines.push("- `hunch_compare(candidates)` — rank N candidate branches/commits by architectural fit (fewest invariant hits).");
37
+ lines.push("- `hunch_conformance()` — does the code still SATISFY recorded intent? (e.g. `pay` still reaches `verifySession`). Run before a refactor.");
37
38
  lines.push("- `hunch_record_decision(...)` — write back a decision after a non-trivial choice.");
38
39
  if (constraints.length) {
39
40
  lines.push("");
@@ -17,6 +17,7 @@ import { buildCorrectionConstraint } from "../core/correction.js";
17
17
  import { revParse, asOfDate, revExists, lastChangeDate, rangeFiles, rangeDiff, commitFiles, commitDiff, stagedFiles, stagedDiff, commitAndPushHunch } from "../extractors/git.js";
18
18
  import { formatContext } from "../core/format.js";
19
19
  import { compareCandidates } from "../core/compare.js";
20
+ import { checkConformance } from "../core/conformance.js";
20
21
  import { renderMarkdown, verdict } from "../core/checkreport.js";
21
22
  import { HUNCH_VERSION } from "../core/version.js";
22
23
  const ok = (text) => ({ content: [{ type: "text", text }] });
@@ -435,6 +436,20 @@ export function buildServer(root) {
435
436
  return err(`Failed to compare candidates: ${e.message}`);
436
437
  }
437
438
  });
439
+ // -- hunch_conformance ----------------------------------------------------
440
+ server.registerTool("hunch_conformance", {
441
+ title: "Does the code still satisfy the recorded intent?",
442
+ description: "Intent-conformance (the inversion of a normal guard): for every in-force decision carrying a conformance predicate, deterministically verify the CODE still satisfies its intent over the dependency graph — e.g. 'pay still reaches verifySession'. Returns the violations: intent the code has silently drifted away from, with NO diff required. Run before a refactor or merge to catch intent erosion a diff-only check can't see.",
443
+ inputSchema: {},
444
+ }, async () => {
445
+ const results = checkConformance(store);
446
+ if (!results.length)
447
+ return ok("No conformance predicates recorded. Add a `conformance` predicate to a decision (e.g. {assert:'calls', subject:'pay', object:'verifySession'}) to prove the code honors its intent.");
448
+ const violations = results.filter((r) => !r.satisfied);
449
+ const lines = results.map((r) => `${r.satisfied ? "✅" : "⛔"} ${r.decision} "${r.title}" — ${r.assert} ${r.subject}${r.object ? ` → ${r.object}` : ""}: ${r.detail}`);
450
+ const head = violations.length ? `⛔ ${violations.length} intent(s) the code no longer satisfies` : "✅ the code satisfies every recorded intent";
451
+ return ok(`Intent-conformance (${results.length - violations.length}/${results.length} satisfied):\n\n${lines.join("\n")}\n\n${head}`);
452
+ });
438
453
  return server;
439
454
  }
440
455
  function provLine(record) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
6
6
  "description": "Hunch — an Engineering Memory OS: a persistent, git-native reasoning graph over a codebase, exposed to Claude Code via MCP.",