@davesheffer/hunch 1.46.0 → 1.47.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 +280 -358
- package/dist/cli/invocation.d.ts +8 -13
- package/dist/cli/invocation.js +26 -29
- package/dist/cli/taskReport.d.ts +6 -0
- package/dist/cli/taskReport.js +31 -8
- 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.d.ts +2 -0
- package/dist/core/correction.js +23 -10
- 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/taskRecord.d.ts +1 -1
- package/dist/core/taskRecord.js +2 -2
- package/dist/core/taskReport.d.ts +8 -3
- package/dist/core/taskReport.js +7 -2
- package/dist/core/taskReportEvidence.d.ts +7 -0
- package/dist/core/taskReportEvidence.js +16 -4
- package/dist/core/taskReportHook.js +2 -2
- package/dist/core/taskReportPublic.js +2 -2
- package/dist/core/taskReportRender.js +2 -2
- 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 +18 -23
- package/dist/extractors/git.js +72 -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/health.js +1 -1
- package/dist/integrations/hooks.d.ts +9 -14
- package/dist/integrations/hooks.js +19 -28
- package/dist/integrations/mergeDriver.js +13 -14
- package/dist/integrations/probe.d.ts +11 -0
- package/dist/integrations/probe.js +29 -4
- package/dist/integrations/sync.d.ts +16 -4
- package/dist/integrations/sync.js +54 -3
- package/dist/integrations/workspaceLedger.d.ts +1 -1
- package/dist/mcp/server.d.ts +4 -5
- package/dist/mcp/server.js +71 -83
- package/dist/mcp/taskReportTools.js +4 -4
- package/dist/serve/app.js +2 -2
- 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/dist/taskReports.d.ts +1 -0
- package/dist/taskReports.js +2 -2
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/core/types.d.ts
CHANGED
|
@@ -111,12 +111,12 @@ export type Resource = z.infer<typeof ResourceSchema>;
|
|
|
111
111
|
export declare const EdgeType: z.ZodEnum<{
|
|
112
112
|
depends_on: "depends_on";
|
|
113
113
|
calls: "calls";
|
|
114
|
+
references: "references";
|
|
114
115
|
imports: "imports";
|
|
115
116
|
contains: "contains";
|
|
116
117
|
implements: "implements";
|
|
117
118
|
supersedes: "supersedes";
|
|
118
119
|
related_to: "related_to";
|
|
119
|
-
references: "references";
|
|
120
120
|
provides: "provides";
|
|
121
121
|
belongs_to: "belongs_to";
|
|
122
122
|
implemented_by: "implemented_by";
|
|
@@ -173,12 +173,12 @@ export declare const EdgeSchema: z.ZodObject<{
|
|
|
173
173
|
type: z.ZodEnum<{
|
|
174
174
|
depends_on: "depends_on";
|
|
175
175
|
calls: "calls";
|
|
176
|
+
references: "references";
|
|
176
177
|
imports: "imports";
|
|
177
178
|
contains: "contains";
|
|
178
179
|
implements: "implements";
|
|
179
180
|
supersedes: "supersedes";
|
|
180
181
|
related_to: "related_to";
|
|
181
|
-
references: "references";
|
|
182
182
|
provides: "provides";
|
|
183
183
|
belongs_to: "belongs_to";
|
|
184
184
|
implemented_by: "implemented_by";
|
|
@@ -783,12 +783,12 @@ export declare const SCHEMAS: {
|
|
|
783
783
|
type: z.ZodEnum<{
|
|
784
784
|
depends_on: "depends_on";
|
|
785
785
|
calls: "calls";
|
|
786
|
+
references: "references";
|
|
786
787
|
imports: "imports";
|
|
787
788
|
contains: "contains";
|
|
788
789
|
implements: "implements";
|
|
789
790
|
supersedes: "supersedes";
|
|
790
791
|
related_to: "related_to";
|
|
791
|
-
references: "references";
|
|
792
792
|
provides: "provides";
|
|
793
793
|
belongs_to: "belongs_to";
|
|
794
794
|
implemented_by: "implemented_by";
|
package/dist/core/types.js
CHANGED
|
@@ -147,12 +147,12 @@ export const ResourceSchema = z.object({
|
|
|
147
147
|
export const EdgeType = z.enum([
|
|
148
148
|
"depends_on",
|
|
149
149
|
"calls",
|
|
150
|
+
"references",
|
|
150
151
|
"imports",
|
|
151
152
|
"contains",
|
|
152
153
|
"implements",
|
|
153
154
|
"supersedes",
|
|
154
155
|
"related_to",
|
|
155
|
-
"references",
|
|
156
156
|
"provides",
|
|
157
157
|
"belongs_to",
|
|
158
158
|
"implemented_by",
|
|
@@ -29,10 +29,10 @@ export interface DiffAnalysis {
|
|
|
29
29
|
removedDeps: string[];
|
|
30
30
|
addedLines: number;
|
|
31
31
|
removedLines: number;
|
|
32
|
-
/** Added line bodies (
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
/** Added line bodies (content after the `+`) per file, for EVERY file, not only
|
|
33
|
+
* substantive ones. The text veto's symbol/pattern tiers match call sites, which
|
|
34
|
+
* `addedSymbols` cannot see. Keyed by the same new-side path as the per-file
|
|
35
|
+
* declarations map. */
|
|
36
36
|
addedLinesByFile: Map<string, string[]>;
|
|
37
37
|
}
|
|
38
38
|
/** Undo git's C-style path quoting. Even with core.quotePath=false, git quotes a
|
package/dist/extractors/diff.js
CHANGED
|
@@ -20,25 +20,21 @@ const DECL_PATTERNS = [
|
|
|
20
20
|
{ kind: "type", re: /^\s*(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\s*[=<]/ },
|
|
21
21
|
{ kind: "const", re: /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/ },
|
|
22
22
|
{ kind: "const", re: /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?function/ },
|
|
23
|
+
// No Python class pattern: the generic class pattern above already matches a
|
|
24
|
+
// Python class header, and the first match wins.
|
|
23
25
|
{ kind: "function", re: /^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/ },
|
|
24
|
-
// No Python-specific class pattern needed: the generic TS `class` pattern above has no
|
|
25
|
-
// trailing-syntax requirement (no `{`/`:`), so it already matches Python's
|
|
26
|
-
// `class Foo(Bar):` header too, and — since declOf() returns on the first match —
|
|
27
|
-
// always wins for Python class lines before any Python-specific pattern would run.
|
|
28
26
|
];
|
|
29
27
|
import { languageFor, isSubstantive } from "./languages.js";
|
|
30
28
|
const IMPORT_RE = /^\s*import\s+(?:[^'"]*from\s+)?['"]([^'"]+)['"]/;
|
|
31
29
|
const CONT_IMPORT_RE = /^\s*\}?\s*from\s+['"]([^'"]+)['"]/; // multi-line: "} from 'x'"
|
|
32
30
|
const REQUIRE_RE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
33
31
|
const PHP_REQUIRE_RE = /\b(?:require|require_once|include|include_once)\s*(?:\(\s*)?['"]([^'"]+)['"]/;
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const PY_FROM_IMPORT_RE = /^\s*from\s+([.\w]+)\s+import\s+/; // "from os import path" / "from . import x"
|
|
41
|
-
const isCode = (p) => !!p && languageFor(p) !== null;
|
|
32
|
+
// A COMPLETE Python import statement: comma-separated dotted modules, each with an
|
|
33
|
+
// optional alias, an optional trailing comment, and nothing else. The anchoring is
|
|
34
|
+
// what rejects TypeScript's `import Foo = Bar.Baz;`.
|
|
35
|
+
const PY_IMPORT_RE = /^\s*import\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)(?:\s+as\s+\w+)?(?:\s*,\s*[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*(?:\s+as\s+\w+)?)*\s*(?:#.*)?$/;
|
|
36
|
+
const PY_FROM_RE = /^\s*from\s+(\.*[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*|\.+)\s+import\b/;
|
|
37
|
+
const isCode = (p) => p.length > 0 && languageFor(p) !== null;
|
|
42
38
|
function declOf(line) {
|
|
43
39
|
for (const { kind, re } of DECL_PATTERNS) {
|
|
44
40
|
const m = re.exec(line);
|
|
@@ -52,7 +48,7 @@ function importOf(line) {
|
|
|
52
48
|
CONT_IMPORT_RE.exec(line) ??
|
|
53
49
|
REQUIRE_RE.exec(line) ??
|
|
54
50
|
PHP_REQUIRE_RE.exec(line) ??
|
|
55
|
-
|
|
51
|
+
PY_FROM_RE.exec(line) ??
|
|
56
52
|
PY_IMPORT_RE.exec(line);
|
|
57
53
|
return m ? m[1] : null;
|
|
58
54
|
}
|
|
@@ -280,15 +276,15 @@ export function analyzeDiff(diff) {
|
|
|
280
276
|
// ---- inside a hunk: content lines ----
|
|
281
277
|
if (raw.startsWith("+")) {
|
|
282
278
|
const body = raw.slice(1);
|
|
283
|
-
// Raw added lines are captured for
|
|
279
|
+
// Raw added lines are captured for every file, before the substantive gate:
|
|
284
280
|
// content-matched constraints and Veto tripwires are not code-only rules
|
|
285
281
|
// (a blocking invariant legitimately scopes .github/workflows/**, *.sql,
|
|
286
282
|
// Dockerfile). Skipping them here left `scopedAdded` empty, which
|
|
287
283
|
// buildCheckReport reads as "cannot prove a violation ⇒ complies" — so the
|
|
288
284
|
// pre-edit hook denied the edit while `hunch check --strict` passed the
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
285
|
+
// same diff. Churn counters below now include prose (isSubstantive, issue
|
|
286
|
+
// #12), while symbol/import extraction stays code-only (isCode): a
|
|
287
|
+
// declaration is a code concept.
|
|
292
288
|
let lines = addedLinesBy.get(curFile);
|
|
293
289
|
if (!lines) {
|
|
294
290
|
lines = [];
|
|
@@ -301,12 +297,12 @@ export function analyzeDiff(diff) {
|
|
|
301
297
|
if (!curAdded && !curDeleted)
|
|
302
298
|
filesModified.add(curFile);
|
|
303
299
|
if (isCode(curFile)) {
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
declsFor(curFile)
|
|
307
|
-
const
|
|
308
|
-
if (
|
|
309
|
-
addedImports.add(
|
|
300
|
+
const decl = declOf(body);
|
|
301
|
+
if (decl)
|
|
302
|
+
declsFor(curFile).added.set(decl.name, decl);
|
|
303
|
+
const spec = importOf(body);
|
|
304
|
+
if (spec && !spec.startsWith("."))
|
|
305
|
+
addedImports.add(spec);
|
|
310
306
|
}
|
|
311
307
|
}
|
|
312
308
|
else if (raw.startsWith("-")) {
|
|
@@ -317,12 +313,12 @@ export function analyzeDiff(diff) {
|
|
|
317
313
|
filesModified.add(curFile);
|
|
318
314
|
if (isCode(curFile)) {
|
|
319
315
|
const body = raw.slice(1);
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
declsFor(curFile)
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
removedImports.add(
|
|
316
|
+
const decl = declOf(body);
|
|
317
|
+
if (decl)
|
|
318
|
+
declsFor(curFile).removed.set(decl.name, decl);
|
|
319
|
+
const spec = importOf(body);
|
|
320
|
+
if (spec && !spec.startsWith("."))
|
|
321
|
+
removedImports.add(spec);
|
|
326
322
|
}
|
|
327
323
|
}
|
|
328
324
|
}
|
package/dist/extractors/git.d.ts
CHANGED
|
@@ -266,6 +266,16 @@ export declare function pathKnownToHistory(root: string, file: string): boolean;
|
|
|
266
266
|
/** Current branch name (e.g. "main", "feat/x"), or "" in detached HEAD / non-repo.
|
|
267
267
|
* Stamped onto auto-captured decisions so branch-scoped work stays filterable. */
|
|
268
268
|
export declare function currentBranch(cwd: string): string;
|
|
269
|
+
/** Whether `cwd` has its repository's default branch checked out: origin/HEAD's branch
|
|
270
|
+
* when the remote names one, else `main` or `master`. False on a detached HEAD or
|
|
271
|
+
* outside a repository, so a caller keeps its pre-existing behavior there. */
|
|
272
|
+
export declare function onDefaultBranch(cwd: string): boolean;
|
|
273
|
+
/** Whether a memory commit of `hunchDir` would have anything to stage: a tracked file git
|
|
274
|
+
* sees as changed (after its own line-ending normalization) or an untracked, non-ignored
|
|
275
|
+
* file, skipping the derived artifacts and the per-machine local.json the commit never
|
|
276
|
+
* stages. A hook's inherited GIT_DIR/GIT_WORK_TREE is stripped, so the repository is
|
|
277
|
+
* resolved from `hunchDir` itself. False when git cannot answer. */
|
|
278
|
+
export declare function hunchDirHasChanges(hunchDir: string): boolean;
|
|
269
279
|
/** Whether HEAD is somewhere a write must not land: mid rebase/merge/cherry-pick/revert/bisect
|
|
270
280
|
* ("git-operation-in-progress", which wins when both hold), or detached ("detached-head").
|
|
271
281
|
* null when HEAD is a settled branch — and also when git cannot answer at all (not a repo,
|
|
@@ -307,32 +317,17 @@ export interface CommitCandidate {
|
|
|
307
317
|
sha: string;
|
|
308
318
|
files: string[];
|
|
309
319
|
}
|
|
310
|
-
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* set of now-orphaned source-branch commits. */
|
|
320
|
+
/** Commits in `oldRef..newRef` (most recent `maxCount`, oldest first), each with the
|
|
321
|
+
* non-deleted paths it touched. Squash-merge provenance repair reads this. A git
|
|
322
|
+
* failure yields []. */
|
|
314
323
|
export declare function mergeRangeChanges(oldRef: string, newRef: string, cwd: string, maxCount?: number): CommitCandidate[];
|
|
315
|
-
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* single git process regardless of how many decisions carry a commit, not
|
|
319
|
-
* one per decision. Returns the subset of `shas` that resolve to a real
|
|
320
|
-
* commit object, or `null` if the check itself failed to run (not a git
|
|
321
|
-
* repo, git missing, timeout) — distinct from an empty set, which means
|
|
322
|
-
* "checked, and none of them resolve." A caller that collapsed those two
|
|
323
|
-
* would flag every commit as unresolvable on a transient environment error
|
|
324
|
-
* instead of failing open. */
|
|
324
|
+
/** Which of `shas` resolve to a commit, via one `git cat-file --batch-check`.
|
|
325
|
+
* Malformed values are dropped before the stdin lines are built (positional mapping).
|
|
326
|
+
* Returns null when git itself could not run: callers fail open. */
|
|
325
327
|
export declare function commitsExist(shas: readonly string[], cwd: string): Set<string> | null;
|
|
326
328
|
export type CommitRepairStatus = "orphaned" | "current" | "unresolvable" | "unknown";
|
|
327
|
-
/**
|
|
328
|
-
* "orphaned"
|
|
329
|
-
* ONLY status planCommitRepair should ever act on. "unresolvable" (the
|
|
330
|
-
* commit doesn't exist here at all) is explicitly out of repair scope —
|
|
331
|
-
* drift.ts's commit-unresolvable kind is what flags that case, because
|
|
332
|
-
* there's no reliable signal left to match a replacement against. "unknown"
|
|
333
|
-
* (the git command itself failed to run — timeout, git missing) must never
|
|
334
|
-
* be treated as repair-eligible; collapsing "false" and "error" into one
|
|
335
|
-
* boolean is exactly the bug this type exists to prevent. */
|
|
329
|
+
/** Whether `commit` is reachable from `ref`. Never collapses "no" and "error": only
|
|
330
|
+
* "orphaned" is repair-eligible. A missing or non-directory cwd is "unresolvable". */
|
|
336
331
|
export declare function commitRepairStatus(commit: string, ref: string, cwd: string): CommitRepairStatus;
|
|
337
332
|
/** commitRepairStatus against several tips: "orphaned" only when NO ref
|
|
338
333
|
* reaches the commit. A historical `--range old..new` must not make a commit
|
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
|
}
|
|
@@ -2289,6 +2283,33 @@ export function currentBranch(cwd) {
|
|
|
2289
2283
|
const b = gitSafe(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
2290
2284
|
return b === "HEAD" ? "" : b; // detached HEAD reports "HEAD" — treat as no branch
|
|
2291
2285
|
}
|
|
2286
|
+
/** Whether `cwd` has its repository's default branch checked out: origin/HEAD's branch
|
|
2287
|
+
* when the remote names one, else `main` or `master`. False on a detached HEAD or
|
|
2288
|
+
* outside a repository, so a caller keeps its pre-existing behavior there. */
|
|
2289
|
+
export function onDefaultBranch(cwd) {
|
|
2290
|
+
const branch = currentBranch(cwd);
|
|
2291
|
+
if (!branch)
|
|
2292
|
+
return false;
|
|
2293
|
+
const remoteHead = gitSafe(["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
2294
|
+
if (remoteHead)
|
|
2295
|
+
return branch === remoteHead.replace(/^origin\//, "");
|
|
2296
|
+
return branch === "main" || branch === "master";
|
|
2297
|
+
}
|
|
2298
|
+
/** Whether a memory commit of `hunchDir` would have anything to stage: a tracked file git
|
|
2299
|
+
* sees as changed (after its own line-ending normalization) or an untracked, non-ignored
|
|
2300
|
+
* file, skipping the derived artifacts and the per-machine local.json the commit never
|
|
2301
|
+
* stages. A hook's inherited GIT_DIR/GIT_WORK_TREE is stripped, so the repository is
|
|
2302
|
+
* resolved from `hunchDir` itself. False when git cannot answer. */
|
|
2303
|
+
export function hunchDirHasChanges(hunchDir) {
|
|
2304
|
+
const changed = [
|
|
2305
|
+
...gitSafeIsolated(["diff", "--name-only", "--relative", "--no-renames", "HEAD", "--", "."], hunchDir).split("\n"),
|
|
2306
|
+
...gitSafeIsolated(["ls-files", "--others", "--exclude-standard", "--", "."], hunchDir).split("\n"),
|
|
2307
|
+
];
|
|
2308
|
+
return changed.some((raw) => {
|
|
2309
|
+
const path = raw.trim().replace(/\\/g, "/");
|
|
2310
|
+
return path !== "" && path !== "local.json" && !isDerivedStoreArtifact(path);
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2292
2313
|
/** The state files git writes while it is replaying history. `git rev-parse --git-path`
|
|
2293
2314
|
* resolves each against THIS worktree's private git dir (a linked worktree keeps its own
|
|
2294
2315
|
* rebase-merge/MERGE_HEAD), so a rebase in one worktree never reads as one in another. */
|
|
@@ -2653,83 +2674,70 @@ export function commitChanges(sha, cwd) {
|
|
|
2653
2674
|
}
|
|
2654
2675
|
return out;
|
|
2655
2676
|
}
|
|
2656
|
-
/** Default cap on
|
|
2657
|
-
*
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
const shas =
|
|
2677
|
+
/** Default cap on commits the range walk inspects: insurance against an unbounded
|
|
2678
|
+
* background hook run when a pull brings in hundreds of commits. */
|
|
2679
|
+
const DEFAULT_RANGE_CAP = 500;
|
|
2680
|
+
/** Commits in `oldRef..newRef` (most recent `maxCount`, oldest first), each with the
|
|
2681
|
+
* non-deleted paths it touched. Squash-merge provenance repair reads this. A git
|
|
2682
|
+
* failure yields []. */
|
|
2683
|
+
export function mergeRangeChanges(oldRef, newRef, cwd, maxCount = DEFAULT_RANGE_CAP) {
|
|
2684
|
+
const raw = gitSafe(["rev-list", "--abbrev-commit", `--max-count=${maxCount}`, `${oldRef}..${newRef}`], cwd);
|
|
2685
|
+
if (!raw)
|
|
2686
|
+
return [];
|
|
2687
|
+
const shas = raw.split("\n").map((s) => s.trim()).filter(Boolean).reverse();
|
|
2667
2688
|
return shas.map((sha) => ({
|
|
2668
2689
|
sha,
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2690
|
+
files: commitChanges(sha, cwd)
|
|
2691
|
+
.filter((c) => c.status !== "deleted")
|
|
2692
|
+
.map((c) => c.after ?? c.before)
|
|
2693
|
+
.filter((f) => !!f),
|
|
2672
2694
|
}));
|
|
2673
2695
|
}
|
|
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. */
|
|
2696
|
+
const HEX_COMMIT_RE = /^[0-9a-f]{7,64}$/i;
|
|
2697
|
+
/** Which of `shas` resolve to a commit, via one `git cat-file --batch-check`.
|
|
2698
|
+
* Malformed values are dropped before the stdin lines are built (positional mapping).
|
|
2699
|
+
* Returns null when git itself could not run: callers fail open. */
|
|
2684
2700
|
export function commitsExist(shas, cwd) {
|
|
2685
|
-
const resolvable = new Set();
|
|
2686
2701
|
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));
|
|
2702
|
+
return new Set();
|
|
2703
|
+
const valid = shas.filter((s) => HEX_COMMIT_RE.test(s));
|
|
2692
2704
|
if (!valid.length)
|
|
2693
|
-
return
|
|
2694
|
-
let
|
|
2705
|
+
return new Set();
|
|
2706
|
+
let out;
|
|
2695
2707
|
try {
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
2700
|
-
timeout: 10_000,
|
|
2708
|
+
out = execFileSync("git", ["-C", cwd, "cat-file", "--batch-check"], {
|
|
2709
|
+
input: valid.map((s) => `${s}^{commit}`).join("\n") + "\n",
|
|
2710
|
+
encoding: "utf8", timeout: 10_000, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024,
|
|
2701
2711
|
});
|
|
2702
2712
|
}
|
|
2703
2713
|
catch {
|
|
2704
2714
|
return null;
|
|
2705
2715
|
}
|
|
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. */
|
|
2716
|
+
const lines = out.split("\n");
|
|
2717
|
+
const found = new Set();
|
|
2718
|
+
valid.forEach((sha, i) => {
|
|
2719
|
+
if (lines[i] !== undefined && !/\bmissing$/.test(lines[i].trim()))
|
|
2720
|
+
found.add(sha);
|
|
2721
|
+
});
|
|
2722
|
+
return found;
|
|
2723
|
+
}
|
|
2724
|
+
/** Whether `commit` is reachable from `ref`. Never collapses "no" and "error": only
|
|
2725
|
+
* "orphaned" is repair-eligible. A missing or non-directory cwd is "unresolvable". */
|
|
2722
2726
|
export function commitRepairStatus(commit, ref, cwd) {
|
|
2723
|
-
if (
|
|
2727
|
+
if (!HEX_COMMIT_RE.test(commit))
|
|
2724
2728
|
return "unresolvable";
|
|
2729
|
+
const opts = { cwd, stdio: "ignore", timeout: 5_000 };
|
|
2725
2730
|
try {
|
|
2726
|
-
execFileSync("git", ["-
|
|
2731
|
+
execFileSync("git", ["cat-file", "-e", `${commit}^{commit}`], opts);
|
|
2727
2732
|
}
|
|
2728
2733
|
catch (e) {
|
|
2729
|
-
|
|
2734
|
+
const err = e;
|
|
2735
|
+
if (typeof err.status === "number" || err.code === "ENOENT" || err.code === "ENOTDIR")
|
|
2736
|
+
return "unresolvable";
|
|
2737
|
+
return "unknown";
|
|
2730
2738
|
}
|
|
2731
2739
|
try {
|
|
2732
|
-
execFileSync("git", ["
|
|
2740
|
+
execFileSync("git", ["merge-base", "--is-ancestor", commit, ref], opts);
|
|
2733
2741
|
return "current";
|
|
2734
2742
|
}
|
|
2735
2743
|
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;
|