@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 +3 -2
- package/dist/core/taskReportRender.d.ts +3 -0
- package/dist/core/taskReportRender.js +22 -12
- package/dist/extractors/indexer.d.ts +5 -0
- package/dist/extractors/indexer.js +17 -2
- package/dist/extractors/parse.d.ts +3 -1
- package/dist/extractors/parse.js +6 -2
- package/dist/store/stateBinding.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
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(`
|
|
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
|
-
|
|
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: ${
|
|
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 · ${
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
81
|
+
lines.push(`Violated ${plain(recordTitle(report, violated))} · rule broken on changed files`);
|
|
72
82
|
if (report.saves.length)
|
|
73
|
-
lines.push(`Saved ${
|
|
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 · ${
|
|
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 ${
|
|
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
|
|
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
|
package/dist/extractors/parse.js
CHANGED
|
@@ -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
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.
|
|
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.
|
|
16
|
+
"version": "1.40.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|