@davesheffer/hunch 1.46.0 → 1.47.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 +151 -321
- package/dist/cli/invocation.d.ts +8 -13
- package/dist/cli/invocation.js +26 -29
- package/dist/constitution/delta.js +2 -2
- package/dist/constitution/sourceMutation.js +28 -18
- package/dist/core/commitrepair.d.ts +43 -138
- package/dist/core/commitrepair.js +76 -167
- package/dist/core/correction.js +9 -9
- package/dist/core/docanchors.js +9 -9
- package/dist/core/drift.js +22 -27
- package/dist/core/escalations.d.ts +16 -58
- package/dist/core/escalations.js +39 -121
- package/dist/core/glob.d.ts +7 -7
- package/dist/core/glob.js +12 -11
- package/dist/core/groundingLag.d.ts +2 -9
- package/dist/core/groundingLag.js +5 -11
- package/dist/core/groundingMerge.d.ts +2 -7
- package/dist/core/groundingMerge.js +35 -54
- package/dist/core/paths.d.ts +17 -24
- package/dist/core/paths.js +37 -43
- package/dist/core/refrepair.d.ts +2 -1
- package/dist/core/refrepair.js +4 -4
- package/dist/core/repairqueue.d.ts +17 -34
- package/dist/core/repairqueue.js +48 -98
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/extractors/diff.d.ts +4 -4
- package/dist/extractors/diff.js +25 -29
- package/dist/extractors/git.d.ts +8 -23
- package/dist/extractors/git.js +45 -64
- package/dist/extractors/helm.d.ts +10 -0
- package/dist/extractors/helm.js +43 -59
- package/dist/extractors/indexer.d.ts +6 -0
- package/dist/extractors/indexer.js +126 -178
- package/dist/extractors/k8sManifest.d.ts +23 -80
- package/dist/extractors/k8sManifest.js +319 -690
- package/dist/extractors/languages.d.ts +28 -37
- package/dist/extractors/languages.js +55 -54
- package/dist/extractors/nativeTreeSitter.d.ts +4 -4
- package/dist/extractors/nativeTreeSitter.js +17 -14
- package/dist/extractors/parse.d.ts +7 -13
- package/dist/extractors/parse.js +39 -51
- package/dist/integrations/claudemd.js +1 -0
- package/dist/integrations/gitignore.js +3 -5
- package/dist/integrations/hooks.d.ts +9 -14
- package/dist/integrations/hooks.js +19 -28
- package/dist/integrations/mergeDriver.js +13 -14
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +60 -73
- package/dist/store/hunchStore.js +12 -18
- package/dist/synthesis/provider.d.ts +19 -21
- package/dist/synthesis/provider.js +67 -76
- package/dist/synthesis/synthesize.d.ts +8 -8
- package/dist/synthesis/synthesize.js +32 -49
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/extractors/git.js
CHANGED
|
@@ -861,9 +861,6 @@ export function headFileContent(root, rel) {
|
|
|
861
861
|
encoding: "utf8",
|
|
862
862
|
env: foreignRepoEnv(process.env),
|
|
863
863
|
maxBuffer: 16 * 1024 * 1024,
|
|
864
|
-
// An untracked/absent-at-HEAD path is an expected, silently-handled case
|
|
865
|
-
// (falls through to the catch below) — don't let git's "fatal: path ...
|
|
866
|
-
// does not exist in 'HEAD'" leak onto the caller's stderr for it.
|
|
867
864
|
stdio: ["ignore", "pipe", "ignore"],
|
|
868
865
|
});
|
|
869
866
|
}
|
|
@@ -940,10 +937,7 @@ function isDerivedStoreArtifact(relativeName) {
|
|
|
940
937
|
// staged alongside the record; treating it as a violation made every served write skip
|
|
941
938
|
// the commit quietly and report durability "local" forever (1.26.0/1.26.1).
|
|
942
939
|
|| relativeName === "write.lock"
|
|
943
|
-
//
|
|
944
|
-
// rejected-match tombstones (repairqueue.ts): clone-local scratch, never a
|
|
945
|
-
// memory record — must never ride a public flush's `git add .` or an
|
|
946
|
-
// overlay's force-add allowlist into shared/pushed memory.
|
|
940
|
+
// Clone-local scratch that must never ride a public flush or an overlay force-add into shared memory.
|
|
947
941
|
|| relativeName === "pending-commit-repairs.json"
|
|
948
942
|
|| relativeName === "dropped-commit-repairs.json";
|
|
949
943
|
}
|
|
@@ -2653,83 +2647,70 @@ export function commitChanges(sha, cwd) {
|
|
|
2653
2647
|
}
|
|
2654
2648
|
return out;
|
|
2655
2649
|
}
|
|
2656
|
-
/** Default cap on
|
|
2657
|
-
*
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
const shas =
|
|
2650
|
+
/** Default cap on commits the range walk inspects: insurance against an unbounded
|
|
2651
|
+
* background hook run when a pull brings in hundreds of commits. */
|
|
2652
|
+
const DEFAULT_RANGE_CAP = 500;
|
|
2653
|
+
/** Commits in `oldRef..newRef` (most recent `maxCount`, oldest first), each with the
|
|
2654
|
+
* non-deleted paths it touched. Squash-merge provenance repair reads this. A git
|
|
2655
|
+
* failure yields []. */
|
|
2656
|
+
export function mergeRangeChanges(oldRef, newRef, cwd, maxCount = DEFAULT_RANGE_CAP) {
|
|
2657
|
+
const raw = gitSafe(["rev-list", "--abbrev-commit", `--max-count=${maxCount}`, `${oldRef}..${newRef}`], cwd);
|
|
2658
|
+
if (!raw)
|
|
2659
|
+
return [];
|
|
2660
|
+
const shas = raw.split("\n").map((s) => s.trim()).filter(Boolean).reverse();
|
|
2667
2661
|
return shas.map((sha) => ({
|
|
2668
2662
|
sha,
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2663
|
+
files: commitChanges(sha, cwd)
|
|
2664
|
+
.filter((c) => c.status !== "deleted")
|
|
2665
|
+
.map((c) => c.after ?? c.before)
|
|
2666
|
+
.filter((f) => !!f),
|
|
2672
2667
|
}));
|
|
2673
2668
|
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
*
|
|
2677
|
-
*
|
|
2678
|
-
* one per decision. Returns the subset of `shas` that resolve to a real
|
|
2679
|
-
* commit object, or `null` if the check itself failed to run (not a git
|
|
2680
|
-
* repo, git missing, timeout) — distinct from an empty set, which means
|
|
2681
|
-
* "checked, and none of them resolve." A caller that collapsed those two
|
|
2682
|
-
* would flag every commit as unresolvable on a transient environment error
|
|
2683
|
-
* instead of failing open. */
|
|
2669
|
+
const HEX_COMMIT_RE = /^[0-9a-f]{7,64}$/i;
|
|
2670
|
+
/** Which of `shas` resolve to a commit, via one `git cat-file --batch-check`.
|
|
2671
|
+
* Malformed values are dropped before the stdin lines are built (positional mapping).
|
|
2672
|
+
* Returns null when git itself could not run: callers fail open. */
|
|
2684
2673
|
export function commitsExist(shas, cwd) {
|
|
2685
|
-
const resolvable = new Set();
|
|
2686
2674
|
if (!shas.length)
|
|
2687
|
-
return
|
|
2688
|
-
|
|
2689
|
-
// embedding its own newline would count as more input lines than entries
|
|
2690
|
-
// in `shas`, shifting every later sha's output line and misclassifying it.
|
|
2691
|
-
const valid = shas.filter((sha) => /^[0-9a-f]{7,64}$/i.test(sha));
|
|
2675
|
+
return new Set();
|
|
2676
|
+
const valid = shas.filter((s) => HEX_COMMIT_RE.test(s));
|
|
2692
2677
|
if (!valid.length)
|
|
2693
|
-
return
|
|
2694
|
-
let
|
|
2678
|
+
return new Set();
|
|
2679
|
+
let out;
|
|
2695
2680
|
try {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
2700
|
-
timeout: 10_000,
|
|
2681
|
+
out = execFileSync("git", ["-C", cwd, "cat-file", "--batch-check"], {
|
|
2682
|
+
input: valid.map((s) => `${s}^{commit}`).join("\n") + "\n",
|
|
2683
|
+
encoding: "utf8", timeout: 10_000, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024,
|
|
2701
2684
|
});
|
|
2702
2685
|
}
|
|
2703
2686
|
catch {
|
|
2704
2687
|
return null;
|
|
2705
2688
|
}
|
|
2706
|
-
const lines =
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
*
|
|
2716
|
-
* commit doesn't exist here at all) is explicitly out of repair scope —
|
|
2717
|
-
* drift.ts's commit-unresolvable kind is what flags that case, because
|
|
2718
|
-
* there's no reliable signal left to match a replacement against. "unknown"
|
|
2719
|
-
* (the git command itself failed to run — timeout, git missing) must never
|
|
2720
|
-
* be treated as repair-eligible; collapsing "false" and "error" into one
|
|
2721
|
-
* boolean is exactly the bug this type exists to prevent. */
|
|
2689
|
+
const lines = out.split("\n");
|
|
2690
|
+
const found = new Set();
|
|
2691
|
+
valid.forEach((sha, i) => {
|
|
2692
|
+
if (lines[i] !== undefined && !/\bmissing$/.test(lines[i].trim()))
|
|
2693
|
+
found.add(sha);
|
|
2694
|
+
});
|
|
2695
|
+
return found;
|
|
2696
|
+
}
|
|
2697
|
+
/** Whether `commit` is reachable from `ref`. Never collapses "no" and "error": only
|
|
2698
|
+
* "orphaned" is repair-eligible. A missing or non-directory cwd is "unresolvable". */
|
|
2722
2699
|
export function commitRepairStatus(commit, ref, cwd) {
|
|
2723
|
-
if (
|
|
2700
|
+
if (!HEX_COMMIT_RE.test(commit))
|
|
2724
2701
|
return "unresolvable";
|
|
2702
|
+
const opts = { cwd, stdio: "ignore", timeout: 5_000 };
|
|
2725
2703
|
try {
|
|
2726
|
-
execFileSync("git", ["-
|
|
2704
|
+
execFileSync("git", ["cat-file", "-e", `${commit}^{commit}`], opts);
|
|
2727
2705
|
}
|
|
2728
2706
|
catch (e) {
|
|
2729
|
-
|
|
2707
|
+
const err = e;
|
|
2708
|
+
if (typeof err.status === "number" || err.code === "ENOENT" || err.code === "ENOTDIR")
|
|
2709
|
+
return "unresolvable";
|
|
2710
|
+
return "unknown";
|
|
2730
2711
|
}
|
|
2731
2712
|
try {
|
|
2732
|
-
execFileSync("git", ["
|
|
2713
|
+
execFileSync("git", ["merge-base", "--is-ancestor", commit, ref], opts);
|
|
2733
2714
|
return "current";
|
|
2734
2715
|
}
|
|
2735
2716
|
catch (e) {
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import type { ParsedSymbolKind } from "./languages.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic token scan over Helm chart template text. Reports `define`
|
|
4
|
+
* blocks as symbols and `include` / `template` references as calls. This is
|
|
5
|
+
* not a Go-template parser: comments and quoting are deliberately ignored.
|
|
6
|
+
* All offsets are JS string (UTF-16) indices into the source.
|
|
7
|
+
*/
|
|
2
8
|
export interface HelmSymbol {
|
|
3
9
|
name: string;
|
|
4
10
|
kind: ParsedSymbolKind;
|
|
11
|
+
/** Index of the first `{` of the opening define action (inclusive). */
|
|
5
12
|
startChar: number;
|
|
13
|
+
/** Index just past the `}}` of the matching end action (exclusive). */
|
|
6
14
|
endChar: number;
|
|
7
15
|
loc: number;
|
|
8
16
|
bodyText: string;
|
|
9
17
|
}
|
|
10
18
|
export interface HelmCall {
|
|
11
19
|
callee: string;
|
|
20
|
+
/** Index of the first letter of the `include` / `template` keyword. */
|
|
12
21
|
atChar: number;
|
|
22
|
+
/** Index just past the closing quote of the name (exclusive). */
|
|
13
23
|
endChar: number;
|
|
14
24
|
member: boolean;
|
|
15
25
|
}
|
package/dist/extractors/helm.js
CHANGED
|
@@ -1,76 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic text scan for Helm's `define`/`include`/`template` Go-template
|
|
3
|
-
* actions — NOT a Go-template parser, and NOT a tree-sitter query. tree-sitter-yaml
|
|
4
|
-
* has no notion of `{{ }}` content: parsing `{{ include "x" . }}` inside a real
|
|
5
|
-
* YAML mapping value produces only stray `{` flow-mapping-open tokens, with the
|
|
6
|
-
* enclosed text lost to ERROR recovery (verified directly against this repo's
|
|
7
|
-
* tree-sitter-yaml bundle). There is nothing for a tree-sitter query to capture,
|
|
8
|
-
* so this runs as a sidecar text scan over the raw source — invoked only for
|
|
9
|
-
* files under a Helm chart (indexer.ts's chart-root detection), never for
|
|
10
|
-
* arbitrary YAML.
|
|
11
|
-
*
|
|
12
|
-
* Known bounded limitation: this is a token scan, not a full parser. A literal
|
|
13
|
-
* `}}` inside a quoted argument, or an include/define-shaped string inside a
|
|
14
|
-
* `{{/* comment *}}`, can misattribute a char range or produce a phantom call.
|
|
15
|
-
* Both are bounded failure modes (a stray reference to a real symbol name, or a
|
|
16
|
-
* slightly-long symbol range) — the same class of accepted limitation
|
|
17
|
-
* `toleratedErrorScopes` documents for the tree-sitter grammars, not a silent gap.
|
|
18
|
-
* The chart-wide `importedFiles` widening this module's output flows through
|
|
19
|
-
* (indexer.ts) also lets a YAML alias resolve to an anchor in a sibling chart
|
|
20
|
-
* file, even though YAML anchors are properly document-scoped; this only fires
|
|
21
|
-
* when the alias has no matching anchor in its own file (i.e. only on input
|
|
22
|
-
* that's already invalid YAML on its own terms), so it's bounded, but it's a
|
|
23
|
-
* real, disclosed side effect of the chart-scoping mechanism, not something to
|
|
24
|
-
* silently rely on.
|
|
25
|
-
*/
|
|
26
1
|
import { MAX_BODY_TEXT_CHARS } from "./parse.js";
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const ACTION = /\{\{-?([\s\S]*?)-?\}\}/g;
|
|
30
|
-
const BLOCK_OPEN = new Set(["if", "range", "with", "define", "block"]);
|
|
31
|
-
const NAME_ARG = /^"([^"]*)"/;
|
|
32
|
-
// Matched against an action's RAW inner text (not just a leading keyword) so
|
|
33
|
-
// `{{ $labels := include "x" . }}` and `{{ if include "x" . }}` are caught,
|
|
34
|
-
// not only the standalone `{{ include "x" . }}` form.
|
|
35
|
-
const CALL_SITE = /\b(?:include|template)\s+"([^"]+)"/g;
|
|
2
|
+
const OPENERS = new Set(["if", "range", "with", "block"]);
|
|
3
|
+
const CALL_RE = /(?<![A-Za-z0-9_])(?:include|template)\s+"([^"]+)"/g;
|
|
36
4
|
export function extractHelmDirectives(source) {
|
|
37
5
|
const symbols = [];
|
|
38
6
|
const calls = [];
|
|
39
7
|
const stack = [];
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
8
|
+
let pos = 0;
|
|
9
|
+
while (pos < source.length) {
|
|
10
|
+
const open = source.indexOf("{{", pos);
|
|
11
|
+
if (open === -1)
|
|
12
|
+
break;
|
|
13
|
+
const close = source.indexOf("}}", open + 2);
|
|
14
|
+
if (close === -1)
|
|
15
|
+
break;
|
|
16
|
+
const innerStart = source[open + 2] === "-" ? open + 3 : open + 2;
|
|
17
|
+
const innerEnd = close - 1 >= innerStart && source[close - 1] === "-" ? close - 1 : close;
|
|
18
|
+
const inner = innerEnd > innerStart ? source.slice(innerStart, innerEnd) : "";
|
|
19
|
+
const actionEnd = close + 2;
|
|
20
|
+
pos = actionEnd;
|
|
21
|
+
// Calls: any action, any keyword.
|
|
22
|
+
CALL_RE.lastIndex = 0;
|
|
23
|
+
for (let m = CALL_RE.exec(inner); m; m = CALL_RE.exec(inner)) {
|
|
24
|
+
const atChar = innerStart + m.index;
|
|
25
|
+
calls.push({ callee: m[1], atChar, endChar: atChar + m[0].length, member: false });
|
|
26
|
+
}
|
|
27
|
+
const trimmed = inner.trim();
|
|
28
|
+
const ws = trimmed.search(/\s/);
|
|
29
|
+
const keyword = ws === -1 ? trimmed : trimmed.slice(0, ws);
|
|
49
30
|
if (keyword === "define") {
|
|
50
|
-
const
|
|
51
|
-
|
|
31
|
+
const rest = trimmed.slice(keyword.length).trimStart();
|
|
32
|
+
let name = null;
|
|
33
|
+
if (rest.startsWith('"')) {
|
|
34
|
+
const q = rest.indexOf('"', 1);
|
|
35
|
+
const candidate = q === -1 ? "" : rest.slice(1, q);
|
|
36
|
+
if (candidate.length > 0)
|
|
37
|
+
name = candidate;
|
|
38
|
+
}
|
|
39
|
+
stack.push({ start: open, name });
|
|
52
40
|
}
|
|
53
|
-
else if (
|
|
54
|
-
|
|
55
|
-
stack.push({ startChar: m.index });
|
|
41
|
+
else if (OPENERS.has(keyword)) {
|
|
42
|
+
stack.push({ start: open, name: null });
|
|
56
43
|
}
|
|
57
44
|
else if (keyword === "end") {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
45
|
+
const frame = stack.pop();
|
|
46
|
+
if (frame && frame.name !== null) {
|
|
47
|
+
const span = source.slice(frame.start, actionEnd);
|
|
60
48
|
symbols.push({
|
|
61
|
-
name:
|
|
49
|
+
name: frame.name,
|
|
62
50
|
kind: "variable",
|
|
63
|
-
startChar:
|
|
64
|
-
endChar,
|
|
65
|
-
loc:
|
|
66
|
-
bodyText:
|
|
51
|
+
startChar: frame.start,
|
|
52
|
+
endChar: actionEnd,
|
|
53
|
+
loc: span.split("\n").length,
|
|
54
|
+
bodyText: span.slice(0, MAX_BODY_TEXT_CHARS),
|
|
67
55
|
});
|
|
68
56
|
}
|
|
69
57
|
}
|
|
70
|
-
for (const call of raw.matchAll(CALL_SITE)) {
|
|
71
|
-
const atChar = innerStart + call.index;
|
|
72
|
-
calls.push({ callee: call[1], atChar, endChar: atChar + call[0].length, member: false });
|
|
73
|
-
}
|
|
74
58
|
}
|
|
75
59
|
return { symbols, calls };
|
|
76
60
|
}
|
|
@@ -68,3 +68,9 @@ export declare function isExtractorEdge(edge: Edge): boolean;
|
|
|
68
68
|
export declare function mergeScannedEdges(stored: Edge[], scanned: Edge[]): Edge[];
|
|
69
69
|
/** Persist one pure scan into the Git-native source of truth. */
|
|
70
70
|
export declare function indexRepo(store: HunchStore, root: string, opts?: IndexRepoOptions): IndexResult;
|
|
71
|
+
/** Build a lookup from a tracked file to the root directory of its nearest Helm
|
|
72
|
+
* chart ("" = repo root), or null. A directory is a chart root when
|
|
73
|
+
* `<dir>/Chart.yaml` is tracked; nested subcharts resolve to their OWN root. This
|
|
74
|
+
* is a conservative approximation of Helm's release-global template namespace
|
|
75
|
+
* (issue #42): a parent including a subchart define is missed, never wrongly linked. */
|
|
76
|
+
export declare function nearestChartRoot(tracked: readonly string[]): (file: string) => string | null;
|