@davesheffer/hunch 0.28.0 → 0.29.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
@@ -29,7 +29,8 @@ import { syncCommit, recordFailure, captureTestRun } from "../synthesis/synthesi
29
29
  import { parseTestReport } from "../extractors/testreport.js";
30
30
  import { selectProvider } from "../synthesis/provider.js";
31
31
  import { isGitRepo, headSha, logSince, lastChangeDate, stagedFiles, commitFiles, asOfDate, stagedDiff, commitDiff, rangeFiles, rangeDiff, rangeSubjects, revExists, commitAndPushHunch, gitUntrackCached } from "../extractors/git.js";
32
- import { runbookId } from "../core/ids.js";
32
+ import { runbookId, decisionId } from "../core/ids.js";
33
+ import { extractInlineIntent } from "../extractors/comments.js";
33
34
  import { renderText, renderMarkdown, reportFailsStrict } from "../core/checkreport.js";
34
35
  import { partitionReview, READY_MIN_GROUNDED } from "../core/reviewqueue.js";
35
36
  import { installPostCommitHook, installPreCommitHook } from "../integrations/hooks.js";
@@ -536,6 +537,67 @@ program
536
537
  console.log(dim(" advisory, deterministic draft — refine the steps/gotchas; surfaced via `hunch query` and MCP."));
537
538
  store.close();
538
539
  });
540
+ // ---- capture-comments (inline intent → graph; addendum #2) ----------------
541
+ program
542
+ .command("capture-comments")
543
+ .description("Capture inline intent comments into the graph: `hunch-why:` → a decision, `hunch-rule:` → a file-scoped constraint. Deterministic + idempotent.")
544
+ .option("--private", "write captured records into the private overlay (HUNCH_PRIVATE_DIR)")
545
+ .action((opts) => {
546
+ const { store, root } = storeFor();
547
+ if (opts.private && !store.hasPrivate) {
548
+ store.close();
549
+ return fail("--private needs HUNCH_PRIVATE_DIR set to a private store");
550
+ }
551
+ const intents = extractInlineIntent(root);
552
+ const now = new Date().toISOString();
553
+ let dec = 0, con = 0;
554
+ for (const it of intents) {
555
+ const ev = [`${it.file}:${it.line}`];
556
+ if (it.kind === "why") {
557
+ const id = decisionId(`inline:${it.file}:${it.text}`);
558
+ const prev = store.recs("decisions").find((d) => d.id === id); // preserve window for idempotent re-capture
559
+ const rec = {
560
+ id, title: it.text, status: "accepted",
561
+ context: `Captured from an inline hunch-why comment (${it.file}:${it.line}).`,
562
+ decision: it.text, consequences: [], alternatives_rejected: [], rejected_tripwires: [],
563
+ related_components: [], related_files: [it.file], supersedes: null, superseded_by: null,
564
+ caused_by_bug: null, commit: null, valid_from: prev?.valid_from ?? now, valid_to: null,
565
+ retired: { symbols: [], deps: [] },
566
+ provenance: { source: "human_confirmed", confidence: 0.9, evidence: ev }, date: prev?.date ?? now,
567
+ };
568
+ if (opts.private)
569
+ store.putPrivate("decisions", rec);
570
+ else
571
+ store.json.put("decisions", rec);
572
+ dec++;
573
+ }
574
+ else {
575
+ const id = constraintId(`inline:${it.file}:${it.text}`);
576
+ const prev = store.recs("constraints").find((c) => c.id === id);
577
+ const rec = {
578
+ id, type: "correctness", statement: it.text, scope: [it.file],
579
+ // Advisory by default — an inline rule never auto-blocks a build; raise severity
580
+ // deliberately if you want enforcement. Keeps day-one zero false-positive rage.
581
+ severity: "warning", enforcement: "advisory_v1",
582
+ rationale: `Captured from an inline hunch-rule comment (${it.file}:${it.line}).`,
583
+ source_decision: null, violations: [], status: "active",
584
+ valid_from: prev?.valid_from ?? now, valid_to: null,
585
+ provenance: { source: "human_confirmed", confidence: 0.9, evidence: ev },
586
+ };
587
+ if (opts.private)
588
+ store.putPrivate("constraints", rec);
589
+ else
590
+ store.json.put("constraints", rec);
591
+ con++;
592
+ }
593
+ }
594
+ store.reindex();
595
+ if (!intents.length)
596
+ console.log("No `hunch-why:` / `hunch-rule:` comments found.");
597
+ else
598
+ console.log(`✓ captured ${dec} decision(s) + ${con} constraint(s) from inline comments${opts.private ? " [private overlay]" : ""}`);
599
+ store.close();
600
+ });
539
601
  // ---- compare (rank N candidate solutions by architectural fit) ------------
540
602
  program
541
603
  .command("compare")
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Inline intent capture (roadmap addendum #2). A developer marks intent right where
3
+ * it lives in the code — `hunch-why: <reason>` (in a comment → a Decision) or
4
+ * `hunch-rule: <invariant>` (→ a file-scoped Constraint) — and Hunch lifts it into
5
+ * the graph, deterministically. The third capture source alongside commit synthesis and
6
+ * correction capture. The tag must follow a comment marker (the slash pair, #, *, --,
7
+ * <!--, ;) so a matching STRING literal in code isn't mistaken for intent. (Line-based,
8
+ * so a tagged line that is itself a string literal can still false-positive — advisory.)
9
+ */
10
+ import { readFileSync, readdirSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { trackedFiles } from "./git.js";
13
+ import { toPosixTarget } from "../core/paths.js";
14
+ const EXTS = [
15
+ ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
16
+ ".py", ".go", ".rb", ".java", ".rs", ".php", ".cs", ".kt", ".swift", ".scala",
17
+ ".c", ".h", ".cc", ".cpp", ".hpp", ".sql", ".sh",
18
+ ];
19
+ const SKIP = new Set(["node_modules", ".git", ".hunch", ".hunch-private", "dist", "build", "out", "vendor", ".next"]);
20
+ const TAG = /(?:\/\/|#|\*|--|<!--|;)\s*hunch-(why|rule)\s*:\s*(.+?)\s*(?:\*\/|-->|$)/i;
21
+ /** Tracked source files (git ls-files); falls back to a bounded walk outside git. */
22
+ function sourceFiles(root) {
23
+ const tracked = trackedFiles(root, EXTS);
24
+ if (tracked.length)
25
+ return tracked;
26
+ const out = [];
27
+ const walk = (dir, rel, depth) => {
28
+ if (depth > 8)
29
+ return;
30
+ let entries;
31
+ try {
32
+ entries = readdirSync(dir, { withFileTypes: true });
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ for (const e of entries) {
38
+ if (e.name.startsWith("."))
39
+ continue;
40
+ const r = rel ? `${rel}/${e.name}` : e.name;
41
+ if (e.isDirectory()) {
42
+ if (!SKIP.has(e.name))
43
+ walk(join(dir, e.name), r, depth + 1);
44
+ }
45
+ else if (EXTS.some((x) => e.name.endsWith(x))) {
46
+ out.push(r);
47
+ }
48
+ }
49
+ };
50
+ walk(root, "", 0);
51
+ return out;
52
+ }
53
+ export function extractInlineIntent(root) {
54
+ const out = [];
55
+ for (const rel of sourceFiles(root)) {
56
+ let content;
57
+ try {
58
+ content = readFileSync(join(root, rel), "utf8");
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ if (!content.includes("hunch-"))
64
+ continue; // cheap skip before the per-line scan
65
+ const file = toPosixTarget(rel);
66
+ const lines = content.split("\n");
67
+ for (let i = 0; i < lines.length; i++) {
68
+ const m = TAG.exec(lines[i]);
69
+ if (m)
70
+ out.push({ kind: m[1].toLowerCase(), text: m[2].trim(), file, line: i + 1 });
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ //# sourceMappingURL=comments.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "0.28.0",
3
+ "version": "0.29.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.",