@davesheffer/hunch 1.40.0 → 1.40.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/dist/cli/index.js CHANGED
@@ -36,7 +36,7 @@ import { publishedStatus } from "../integrations/registry.js";
36
36
  import { HunchStore } from "../store/hunchStore.js";
37
37
  import { JsonStore } from "../store/jsonStore.js";
38
38
  import { selectEmbedder } from "../store/embedder.js";
39
- import { assertCompleteRepoScan, indexRepo, mergeScannedEdges, scanRepo } from "../extractors/indexer.js";
39
+ import { assertCompleteRepoScan, assertNoTotalParseFailure, indexRepo, mergeScannedEdges, scanRepo } from "../extractors/indexer.js";
40
40
  // Importing this module is free (the addon load is a function call, not a
41
41
  // top-level side effect); only `doctor` below actually calls the loader.
42
42
  import { isParserLoadError, loadNativeTreeSitter } from "../extractors/nativeTreeSitter.js";
@@ -7227,11 +7227,12 @@ program
7227
7227
  // past the isolation guard) is invisible until an index run refuses.
7228
7228
  try {
7229
7229
  loadNativeTreeSitter();
7230
+ assertNoTotalParseFailure(scanRepo(store, root, { churn: false }));
7230
7231
  console.log(`parser: native tree-sitter addons load`);
7231
7232
  }
7232
7233
  catch (e) {
7233
7234
  console.log(`parser: ⛔ ${e.message}`);
7234
- console.log(dim(` no file can be parsed — \`hunch index\` refuses rather than emptying the graph`));
7235
+ console.log(dim(` \`hunch index\` refuses rather than replacing the graph after a parser failure`));
7235
7236
  process.exitCode = 1;
7236
7237
  }
7237
7238
  const c = store.reindex().counts;
@@ -1,5 +1,8 @@
1
1
  import { type LessonHistory, type TaskReport } from "./taskReport.js";
2
2
  export declare function writeTaskReportHtml(root: string, taskId: string, publicOnly?: boolean): string;
3
+ /** Delivered lessons are named on the card up to this many; beyond it the
4
+ * count of the rest is stated, never a silent cut. */
5
+ export declare const CARD_RECALLED_LIMIT = 8;
3
6
  /** One short line for the first time a lesson reaches a task; null when every
4
7
  * delivered revision was already seen in this task. Never a banner per delivery. */
5
8
  export declare function renderRecalledLine(fresh: readonly {
@@ -21,8 +21,15 @@ export function writeTaskReportHtml(root, taskId, publicOnly = false) {
21
21
  writeFileAtomic(file, publicOnly ? renderPublicTaskReportHtml(publicTaskReport(root, taskId)) : renderTaskReportHtml(report, undefined, histories));
22
22
  return file;
23
23
  }
24
+ /** Card text is copied verbatim into an agent's final response, so every
25
+ * field is shown whole: a title cut mid-word with an ellipsis reads as a
26
+ * truncated tool result and the agent refuses to reproduce the card. Every
27
+ * rendered field is schema-bounded (task title and check label ≤ 200
28
+ * characters, record titles ≤ 500), so whole never means unbounded. */
24
29
  function plain(value) { return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim(); }
25
- function clip(value, size = 66) { const chars = [...plain(value)]; return chars.length > size ? chars.slice(0, size - 1).join("") + "…" : chars.join(""); }
30
+ /** Delivered lessons are named on the card up to this many; beyond it the
31
+ * count of the rest is stated, never a silent cut. */
32
+ export const CARD_RECALLED_LIMIT = 8;
26
33
  function uniqueRecords(report) {
27
34
  return [...new Map(report.deliveries.flatMap(d => d.records).map(r => [`${r.kind}:${r.record_id}:${r.content_hash}`, r])).values()];
28
35
  }
@@ -42,41 +49,44 @@ export function renderRecalledLine(fresh) {
42
49
  if (!fresh.length)
43
50
  return null;
44
51
  const rest = fresh.length - 1;
45
- return `Hunch recalled: ${clip(fresh[0].title, 90)}${rest ? ` (+${rest} more lesson${rest === 1 ? "" : "s"})` : ""}`;
52
+ return `Hunch recalled: ${plain(fresh[0].title)}${rest ? ` (+${rest} more lesson${rest === 1 ? "" : "s"})` : ""}`;
46
53
  }
47
54
  export function renderTaskReport(report) {
48
55
  const records = uniqueRecords(report);
49
- const lines = [`Hunch · ${clip(report.task.title)}`, `Task ${report.task.task_id} · ${report.task.state}`];
56
+ const lines = [`Hunch · ${plain(report.task.title)}`, `Task ${report.task.task_id} · ${report.task.state}`];
50
57
  if (!report.deliveries.length)
51
58
  lines.push("No task-linked delivery observed; connection/use is unverified.");
52
59
  else if (!records.length)
53
60
  lines.push(report.coverage === "no-relevant-memory" ? "No relevant memory returned for this task." : "Memory delivered; exact record snapshots unavailable.");
54
61
  else {
55
- lines.push(`Recalled ${clip(records[0].title, 65)}`);
56
- if (records.length > 1)
57
- lines.push(` ${records.length - 1} more lesson(s) in the evidence view`);
62
+ const named = records.slice(0, CARD_RECALLED_LIMIT);
63
+ lines.push(`Recalled ${plain(named[0].title)}`);
64
+ for (const record of named.slice(1))
65
+ lines.push(` ${plain(record.title)}`);
66
+ if (records.length > named.length)
67
+ lines.push(` ${records.length - named.length} more lesson(s) in the evidence view`);
58
68
  }
59
69
  const standing = ruleStanding(report);
60
70
  const violated = standing.find(r => r.outcome === "violated");
61
71
  const held = standing.find(r => r.outcome === "satisfied" && r.current);
62
72
  if (report.claims.length) {
63
73
  const claim = report.claims.find(c => c.supported_by) ?? report.claims[0];
64
- lines.push(`Applied ${clip(claim.action, 42)} · ${claim.supported_by ? "rule-supported" : "agent-reported"}`);
74
+ lines.push(`Applied ${plain(claim.action)} · ${claim.supported_by ? "rule-supported" : "agent-reported"}`);
65
75
  }
66
76
  else if (held)
67
- lines.push(`Conformed ${clip(recordTitle(report, held), 34)} · rule held on ${held.files.length} changed file(s)`);
77
+ lines.push(`Conformed ${plain(recordTitle(report, held))} · rule held on ${held.files.length} changed file(s)`);
68
78
  else if (records.length)
69
79
  lines.push("Impact Memory delivered; contribution unverified.");
70
80
  if (violated)
71
- lines.push(`Violated ${clip(recordTitle(report, violated), 34)} · rule broken on changed files`);
81
+ lines.push(`Violated ${plain(recordTitle(report, violated))} · rule broken on changed files`);
72
82
  if (report.saves.length)
73
- lines.push(`Saved ${clip(report.saves[0].record.title, 38)} · ${report.saves[0].durability}`);
83
+ lines.push(`Saved ${plain(report.saves[0].record.title)} · ${report.saves[0].durability}`);
74
84
  if (report.refusals.length)
75
- lines.push(`Guarded Denial emitted · ${clip(report.refusals[0].record_id, 40)}`);
85
+ lines.push(`Guarded Denial emitted · ${plain(report.refusals[0].record_id)}`);
76
86
  const check = report.checks.at(-1);
77
87
  if (check) {
78
88
  const state = check.cancelled ? "cancelled" : check.timed_out ? "timed out" : check.exit_code === 0 ? "passed" : "failed";
79
- lines.push(`Checked ${clip(check.label, 28)} · ${state}${check.current ? " · current source snapshot" : " · current source unverified"}`);
89
+ lines.push(`Checked ${plain(check.label)} · ${state}${check.current ? " · current source snapshot" : " · current source unverified"}`);
80
90
  }
81
91
  else
82
92
  lines.push("Checked No independent command result recorded.");
@@ -46,6 +46,11 @@ export interface IndexRepoOptions {
46
46
  * false satisfied receipt. Call this after scanRepo when the consumer cannot
47
47
  * represent partial-graph uncertainty directly. */
48
48
  export declare function assertCompleteRepoScan(scan: RepoScan): void;
49
+ /** A whole-language failure can be a broken grammar, not a set of bad files.
50
+ * Refuse the entire publication so cross-language edges and curated components
51
+ * remain consistent with the previous symbols. Read-only scans retain coverage
52
+ * and issues for diagnostics; a valid empty scan is still publishable. */
53
+ export declare function assertNoTotalParseFailure(scan: RepoScan): void;
49
54
  /** Derive the current repository graph without writing JSON or rebuilding SQLite.
50
55
  * Read-only gates use this so checking changed code can never rewrite or publish
51
56
  * the durable graph merely by inspecting it. Existing public graph records remain
@@ -35,6 +35,20 @@ export function assertCompleteRepoScan(scan) {
35
35
  const more = scan.issues.length > 5 ? ` (+${scan.issues.length - 5} more)` : "";
36
36
  throw new Error(`incomplete semantic source scan rejected ${scan.issues.length} file(s): ${sample}${more}`);
37
37
  }
38
+ /** A whole-language failure can be a broken grammar, not a set of bad files.
39
+ * Refuse the entire publication so cross-language edges and curated components
40
+ * remain consistent with the previous symbols. Read-only scans retain coverage
41
+ * and issues for diagnostics; a valid empty scan is still publishable. */
42
+ export function assertNoTotalParseFailure(scan) {
43
+ const failed = scan.result.coverage.filter((item) => item.eligible > 0 && item.reasons.parse_failed === item.eligible);
44
+ if (!failed.length)
45
+ return;
46
+ const details = failed.map((item) => {
47
+ const first = scan.issues.find((issue) => issue.code === "parse_failed" && languageFor(issue.path)?.id === item.language);
48
+ return `${item.language}: all ${item.eligible} eligible file(s) failed to parse; ${first?.detail ?? "unknown parse failure"}`;
49
+ });
50
+ throw new Error(`index refused — ${details.join("; ")}. Previous graph preserved.`);
51
+ }
38
52
  /** Derive the current repository graph without writing JSON or rebuilding SQLite.
39
53
  * Read-only gates use this so checking changed code can never rewrite or publish
40
54
  * the durable graph merely by inspecting it. Existing public graph records remain
@@ -150,7 +164,7 @@ export function scanRepo(store, root, opts = {}) {
150
164
  // one bad/oversized file must never abort the whole index run
151
165
  let parsed;
152
166
  try {
153
- parsed = parseSource(rel, src);
167
+ parsed = parseSource(rel, src, { throwOnParseError: true });
154
168
  }
155
169
  catch (error) {
156
170
  // …but a dead PARSER is not a bad file. The native addons load on first
@@ -163,7 +177,7 @@ export function scanRepo(store, root, opts = {}) {
163
177
  if (isParserLoadError(error))
164
178
  throw error;
165
179
  skipped++;
166
- issues.push({ path: rel, code: "parse_failed", detail: `${rel} could not be parsed` });
180
+ issues.push({ path: rel, code: "parse_failed", detail: `${rel} could not be parsed: ${error instanceof Error ? error.message : String(error)}` });
167
181
  noteSkip(rel, "parse_failed");
168
182
  continue;
169
183
  }
@@ -619,6 +633,7 @@ export function indexRepo(store, root, opts = {}) {
619
633
  ? { kind: "commit", ref: "HEAD" }
620
634
  : opts.source;
621
635
  const scan = scanRepo(store, root, { churn: opts.churn, source });
636
+ assertNoTotalParseFailure(scan);
622
637
  if (opts.requireComplete)
623
638
  assertCompleteRepoScan(scan);
624
639
  store.json.replaceAll("symbols", scan.symbols);
@@ -34,7 +34,9 @@ export interface ParsedFile {
34
34
  /** Cap on a stored symbol's bodyText — large enough for review context, small
35
35
  * enough that a huge function/file doesn't bloat every JSON symbol record. */
36
36
  export declare const MAX_BODY_TEXT_CHARS = 4000;
37
- export declare function parseSource(file: string, source: string): ParsedFile | null;
37
+ export declare function parseSource(file: string, source: string, opts?: {
38
+ throwOnParseError?: boolean;
39
+ }): ParsedFile | null;
38
40
  /** Map each call site to the innermost symbol whose byte-range contains it.
39
41
  * Keyed by the symbol's position (index) in `parsed.symbols` — NOT its
40
42
  * startByte, which is not a reliable per-symbol identity: a language whose
@@ -23,7 +23,7 @@ const STR_QUOTES = /^['"`]|['"`]$/g;
23
23
  /** Cap on a stored symbol's bodyText — large enough for review context, small
24
24
  * enough that a huge function/file doesn't bloat every JSON symbol record. */
25
25
  export const MAX_BODY_TEXT_CHARS = 4000;
26
- export function parseSource(file, source) {
26
+ export function parseSource(file, source, opts = {}) {
27
27
  const spec = languageFor(file);
28
28
  if (!spec)
29
29
  return null;
@@ -49,7 +49,11 @@ export function parseSource(file, source) {
49
49
  try {
50
50
  tree = parser.parse(source, undefined, { bufferSize: Math.max(32 * 1024, source.length * 2 + 1024) });
51
51
  }
52
- catch {
52
+ catch (error) {
53
+ // Index scans need the underlying diagnostic for whole-language failures.
54
+ // Other callers retain the historical best-effort null result.
55
+ if (opts.throwOnParseError)
56
+ throw error;
53
57
  return null;
54
58
  }
55
59
  const symbols = [];
@@ -1036,7 +1036,7 @@ export function subscribeState(store, input, options = {}) {
1036
1036
  const access = createStateAccess(store, request.principal, options);
1037
1037
  const visibilityFiltered = ledger.events.some(e => e.visibility !== undefined) || access.restricted;
1038
1038
  const filtered = !!(facets || subjects || visibilityFiltered);
1039
- const resync = request.after_seq < ledger.floor_seq;
1039
+ const resync = request.after_seq < ledger.floor_seq || request.after_seq > ledger.head_seq;
1040
1040
  const after = resync ? ledger.floor_seq : request.after_seq;
1041
1041
  const events = ledger.events.filter((e) => e.seq > after
1042
1042
  && (!visibilityFiltered || (visibilityAllows(e, request.principal.id) && access.canRead(findRecord(store, e.record_id)?.record) && access.referencesVisible(e)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davesheffer/hunch",
3
- "version": "1.40.0",
3
+ "version": "1.40.1",
4
4
  "mcpName": "io.github.davesheffer/hunch",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dave Sheffer <dave.sheffer1@gmail.com>",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://www.hunchmemory.com",
10
- "version": "1.40.0",
10
+ "version": "1.40.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.40.0",
16
+ "version": "1.40.1",
17
17
  "runtimeHint": "npx",
18
18
  "packageArguments": [
19
19
  {