@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/core/paths.js
CHANGED
|
@@ -3,12 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
5
|
export const HUNCH_DIR = ".hunch";
|
|
6
|
-
/**
|
|
7
|
-
* path with "/"
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* repo-relative — an absolute path passes through with its separators flipped,
|
|
6
|
+
/** Convert a user- or agent-supplied path/target to forward-slash form. Hunch
|
|
7
|
+
* stores every path with "/" (git emits it on every OS), so a Windows-style
|
|
8
|
+
* backslash path would otherwise never match the stored form. Harmless on
|
|
9
|
+
* symbol names, which carry no backslashes. It does NOT make a path
|
|
10
|
+
* repo-relative: an absolute path only has its separators flipped and is left
|
|
12
11
|
* unchanged otherwise; use `repoRelativeTarget` for that.
|
|
13
12
|
*
|
|
14
13
|
* Inherently ambiguous for a string like `docs/notes\notes.md`: it could be a
|
|
@@ -21,47 +20,42 @@ export const HUNCH_DIR = ".hunch";
|
|
|
21
20
|
export function toPosixTarget(target) {
|
|
22
21
|
return target.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
23
22
|
}
|
|
24
|
-
/**
|
|
25
|
-
* to
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
/** Canonical real path of `p`, tolerant of paths that do not exist yet (a file
|
|
24
|
+
* about to be written, a glob with non-literal segments): the longest existing
|
|
25
|
+
* ancestor is resolved through the native realpath and the missing trailing
|
|
26
|
+
* segments are re-appended unchanged. Terminates at the filesystem root,
|
|
27
|
+
* is idempotent on a resolved path and never throws for a missing one. */
|
|
28
28
|
export function realpathNorm(p) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const tail = [];
|
|
30
|
+
let cur = p;
|
|
31
|
+
for (;;) {
|
|
32
|
+
try {
|
|
33
|
+
const real = realpathSync.native(cur);
|
|
34
|
+
return tail.length ? join(real, ...tail.reverse()) : real;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
const parent = dirname(cur);
|
|
38
|
+
if (parent === cur)
|
|
39
|
+
return p;
|
|
40
|
+
tail.push(basename(cur));
|
|
41
|
+
cur = parent;
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
|
-
/** Rewrite an ABSOLUTE target to repo-relative
|
|
40
|
-
* `root`. Hunch
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* BOTH ends are realpath-normalized before the relative-path computation: on
|
|
48
|
-
* macOS `process.cwd()` (hence `findRoot`) resolves /var → /private/var, but a
|
|
49
|
-
* hook event's `file_path` arrives UN-resolved, so a naive `relative()` yields a
|
|
50
|
-
* bogus "../" path under a symlinked root (/var, /tmp, symlinked $HOME) and the
|
|
51
|
-
* caller treats the file as outside the repo, silently dropping all context
|
|
52
|
-
* (dec_e0a36efbf5). */
|
|
45
|
+
/** Rewrite an ABSOLUTE `target` to repo-relative forward-slash form when it lies
|
|
46
|
+
* inside `root`. Stored Hunch paths are always repo-relative, so an absolute
|
|
47
|
+
* path lifted from an edit payload otherwise matches nothing (issue #296).
|
|
48
|
+
* Both ends go through `realpathNorm`, so a symlinked root (macOS /var vs
|
|
49
|
+
* /private/var, /tmp, a symlinked home) cancels out (dec_e0a36efbf5). Anything
|
|
50
|
+
* with no safe repo-relative form (the root itself, an escape, a drive-letter
|
|
51
|
+
* remainder) is returned in its normalized form unchanged. */
|
|
53
52
|
export function repoRelativeTarget(target, root) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// so an un-posix'd root and a posix'd target would decompose into differently-shaped
|
|
61
|
-
// ancestor chains and never share a common realpath'd prefix.
|
|
62
|
-
const rel = toPosixTarget(relative(realpathNorm(toPosixTarget(root)), realpathNorm(t)));
|
|
63
|
-
if (!rel || rel === ".." || rel.startsWith("../") || isAbsolute(rel) || /^[a-zA-Z]:/.test(rel))
|
|
64
|
-
return t;
|
|
53
|
+
const norm = toPosixTarget(target);
|
|
54
|
+
if (!isAbsolute(norm) && !/^[A-Za-z]:/.test(norm))
|
|
55
|
+
return norm;
|
|
56
|
+
const rel = relative(realpathNorm(toPosixTarget(root)), realpathNorm(norm)).replace(/\\/g, "/");
|
|
57
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel) || /^[A-Za-z]:/.test(rel))
|
|
58
|
+
return norm;
|
|
65
59
|
return rel;
|
|
66
60
|
}
|
|
67
61
|
/** True when `target` (already repo-relative POSIX form) names a regular FILE
|
package/dist/core/refrepair.d.ts
CHANGED
|
@@ -12,10 +12,11 @@ export interface DecisionReferenceRepair {
|
|
|
12
12
|
relatedFiles: number;
|
|
13
13
|
evidence: number;
|
|
14
14
|
}
|
|
15
|
-
export
|
|
15
|
+
export type ExactReplacement = {
|
|
16
16
|
values: string[];
|
|
17
17
|
changed: number;
|
|
18
18
|
};
|
|
19
|
+
export declare function replaceExact(refs: string[], target: string, replacement: string): ExactReplacement;
|
|
19
20
|
/**
|
|
20
21
|
* Return a corrected copy of a decision, or `null` when the source reference is
|
|
21
22
|
* not present. The decision's semantic content and verification timestamp are
|
package/dist/core/refrepair.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export function replaceExact(
|
|
1
|
+
export function replaceExact(refs, target, replacement) {
|
|
2
2
|
let changed = 0;
|
|
3
|
-
const replaced =
|
|
4
|
-
if (value !==
|
|
3
|
+
const replaced = refs.map((value) => {
|
|
4
|
+
if (value !== target)
|
|
5
5
|
return value;
|
|
6
6
|
changed++;
|
|
7
|
-
return
|
|
7
|
+
return replacement;
|
|
8
8
|
});
|
|
9
9
|
// A decision may already cite the destination. Keep the reference list a set
|
|
10
10
|
// after the repair so a correction cannot create duplicate scope/evidence.
|
|
@@ -1,40 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
* is local scratch state, never a source of truth worth failing loudly over. */
|
|
1
|
+
import type { CommitRewrite, DroppedRewrite } from "./commitrepair.js";
|
|
2
|
+
/** The raw queue. Missing, corrupt or non-array files read as `[]`; never throws. */
|
|
4
3
|
export declare function readPendingRepairs(root: string): CommitRewrite[];
|
|
4
|
+
/** Atomically replace the queue. Writing `[]` clears it. */
|
|
5
5
|
export declare function writePendingRepairs(root: string, rewrites: readonly CommitRewrite[]): void;
|
|
6
|
-
/**
|
|
6
|
+
/** The tombstones of rewrites rejected with `--drop`. Same tolerance as the queue. */
|
|
7
7
|
export declare function readDroppedRepairs(root: string): DroppedRewrite[];
|
|
8
|
+
/** Atomically replace the tombstone file. Writing `[]` clears it. */
|
|
8
9
|
export declare function writeDroppedRepairs(root: string, dropped: readonly DroppedRewrite[]): void;
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
* `repair-provenance`'s own action is the one exception: it needs the RAW
|
|
16
|
-
* read (via readPendingRepairs) because it diffs raw-vs-swept to report what
|
|
17
|
-
* it cleaned up, and it persists the sweep back to disk. Every other
|
|
18
|
-
* reader — `hunch escalations`, SessionStart orientation, the `hunch_now`
|
|
19
|
-
* and `hunch_escalations` MCP tools — must call this instead, or a
|
|
20
|
-
* tombstoned entry that hasn't yet been swept by a `repair-provenance` run
|
|
21
|
-
* re-surfaces as if the human never answered it. */
|
|
10
|
+
/**
|
|
11
|
+
* The raw queue minus anything tombstoned. Read-only: it never changes either
|
|
12
|
+
* file. Every read-only consumer uses this -- `hunch escalations`, SessionStart
|
|
13
|
+
* orientation, and the `hunch_now` and `hunch_escalations` MCP tools. Only
|
|
14
|
+
* `repair-provenance` itself reads the raw queue.
|
|
15
|
+
*/
|
|
22
16
|
export declare function readActivePendingRepairs(root: string): CommitRewrite[];
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
* `--apply --only <id>` that's guaranteed to no-op forever. `null` from
|
|
30
|
-
* commitsExist (the check itself failed to run) is treated as fail-open —
|
|
31
|
-
* same discipline as the apply path — so nothing is misreported as
|
|
32
|
-
* withheld just because the check couldn't run.
|
|
33
|
-
*
|
|
34
|
-
* Returns the OBJECTS, not their ids, and every caller must pass the exact
|
|
35
|
-
* same `queued` array into both this function and whatever it hands the
|
|
36
|
-
* result to (commitRepairEscalations) — an id-keyed set would collapse a
|
|
37
|
-
* corrupted queue's two same-id entries (one resolvable, one not) into one
|
|
38
|
-
* unit, exactly the identity confusion withheldForUnresolvableTo's own
|
|
39
|
-
* docstring warns about. */
|
|
17
|
+
/**
|
|
18
|
+
* The queued entries whose proposed `to` does not resolve in this repository.
|
|
19
|
+
* Fails open (nothing withheld) when git cannot answer. The set holds the
|
|
20
|
+
* caller's own objects, so callers must pass the same `queued` array here and to
|
|
21
|
+
* whatever consumes the result.
|
|
22
|
+
*/
|
|
40
23
|
export declare function withheldRewrites(root: string, queued: readonly CommitRewrite[]): Set<CommitRewrite>;
|
package/dist/core/repairqueue.js
CHANGED
|
@@ -1,131 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local-only queue of commit-provenance repairs
|
|
3
|
-
* (typically by the post-merge hook, src/integrations/hooks.ts) but not yet
|
|
4
|
-
* applied. `.hunch/pending-commit-repairs.json` is gitignored the same way
|
|
5
|
-
* `.hunch/local.json` is — never committed, never pushed. It exists because
|
|
6
|
-
* the opportunistic detection window is narrow: `ORIG_HEAD` gets overwritten
|
|
7
|
-
* by the next merge, and the matched-away commit can eventually be gc'd —
|
|
8
|
-
* queuing the exact {id, from, to} triple here lets a human confirm the match
|
|
9
|
-
* later (`hunch repair-provenance --apply`) without losing it.
|
|
2
|
+
* Local-only queue of detected-but-not-yet-applied commit-provenance repairs.
|
|
10
3
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
4
|
+
* It is filled opportunistically, usually by the post-merge hook, and is
|
|
5
|
+
* gitignored like `.hunch/local.json` -- never committed. It exists because the
|
|
6
|
+
* detection window is narrow: `ORIG_HEAD` gets overwritten and the matched-away
|
|
7
|
+
* commit can be garbage-collected. A human confirms later with
|
|
8
|
+
* `hunch repair-provenance --apply`. A sibling tombstone file records the exact
|
|
9
|
+
* `{id, from, to}` triples rejected with `--drop`, so re-detection does not
|
|
10
|
+
* re-queue them.
|
|
18
11
|
*/
|
|
19
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
20
13
|
import { join } from "node:path";
|
|
21
14
|
import { writeFileAtomic } from "./io.js";
|
|
22
15
|
import { withoutDropped, withheldForUnresolvableTo } from "./commitrepair.js";
|
|
23
16
|
import { commitsExist } from "../extractors/git.js";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
const queuePath = (root) => join(root, ".hunch", "pending-commit-repairs.json");
|
|
18
|
+
const droppedPath = (root) => join(root, ".hunch", "dropped-commit-repairs.json");
|
|
19
|
+
const nonEmpty = (v) => typeof v === "string" && v.length > 0;
|
|
20
|
+
/**
|
|
21
|
+
* Shape-only validation: non-empty string id/from/to, and `to !== from` (a no-op
|
|
22
|
+
* is rejected). There is deliberately no hex check -- a bad `to` is caught later
|
|
23
|
+
* by the apply-time existence check, so this loader stays shape-only.
|
|
24
|
+
*/
|
|
25
|
+
function isRewrite(v) {
|
|
26
|
+
if (typeof v !== "object" || v === null)
|
|
32
27
|
return false;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
// and a no-op entry would apply "successfully" while accomplishing nothing.
|
|
36
|
-
// Deliberately NOT a hex-shape check: from/to are opaque strings elsewhere
|
|
37
|
-
// in this codebase's test fixtures, and repairDecisionCommit's own
|
|
38
|
-
// `d.commit === mine.from` guard already makes a garbage `from` inert (it
|
|
39
|
-
// can never match a real decision). A garbage `to` is caught elsewhere,
|
|
40
|
-
// not here: `hunch repair-provenance --apply` runs commitsExist against
|
|
41
|
-
// every `to` it's actually about to act on (the whole queue, or just the
|
|
42
|
-
// one entry `--only <id>` targets) before repairDecisionCommit ever sees
|
|
43
|
-
// it, and leaves a non-resolving entry queued rather than applying it
|
|
44
|
-
// (withheldForUnresolvableTo, src/core/commitrepair.ts) — with the caveat
|
|
45
|
-
// that commitsExist itself fails OPEN if the check can't run at all (not a
|
|
46
|
-
// git repo, git missing, timeout), same as drift.ts's own use of it. This
|
|
47
|
-
// filter stays shape-only on purpose — the existence check needs a
|
|
48
|
-
// repository to run against, which this read-only queue loader doesn't
|
|
49
|
-
// have.
|
|
50
|
-
return typeof id === "string" && !!id
|
|
51
|
-
&& typeof from === "string" && !!from
|
|
52
|
-
&& typeof to === "string" && !!to
|
|
53
|
-
&& to !== from;
|
|
28
|
+
const r = v;
|
|
29
|
+
return nonEmpty(r.id) && nonEmpty(r.from) && nonEmpty(r.to) && r.to !== r.from;
|
|
54
30
|
}
|
|
55
|
-
function
|
|
56
|
-
if (
|
|
31
|
+
function isDropped(v) {
|
|
32
|
+
if (typeof v !== "object" || v === null)
|
|
57
33
|
return false;
|
|
58
|
-
const
|
|
59
|
-
return
|
|
60
|
-
&& typeof from === "string" && !!from
|
|
61
|
-
&& typeof to === "string" && !!to;
|
|
34
|
+
const r = v;
|
|
35
|
+
return nonEmpty(r.id) && nonEmpty(r.from) && nonEmpty(r.to);
|
|
62
36
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export function readPendingRepairs(root) {
|
|
66
|
-
const path = queuePath(root);
|
|
67
|
-
if (!existsSync(path))
|
|
37
|
+
function readList(file, guard) {
|
|
38
|
+
if (!existsSync(file))
|
|
68
39
|
return [];
|
|
69
40
|
try {
|
|
70
|
-
const parsed = JSON.parse(readFileSync(
|
|
71
|
-
return Array.isArray(parsed) ? parsed.filter(
|
|
41
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
42
|
+
return Array.isArray(parsed) ? parsed.filter(guard) : [];
|
|
72
43
|
}
|
|
73
44
|
catch {
|
|
74
45
|
return [];
|
|
75
46
|
}
|
|
76
47
|
}
|
|
48
|
+
/** The raw queue. Missing, corrupt or non-array files read as `[]`; never throws. */
|
|
49
|
+
export function readPendingRepairs(root) {
|
|
50
|
+
return readList(queuePath(root), isRewrite);
|
|
51
|
+
}
|
|
52
|
+
/** Atomically replace the queue. Writing `[]` clears it. */
|
|
77
53
|
export function writePendingRepairs(root, rewrites) {
|
|
78
54
|
writeFileAtomic(queuePath(root), JSON.stringify(rewrites, null, 2) + "\n");
|
|
79
55
|
}
|
|
80
|
-
/**
|
|
56
|
+
/** The tombstones of rewrites rejected with `--drop`. Same tolerance as the queue. */
|
|
81
57
|
export function readDroppedRepairs(root) {
|
|
82
|
-
|
|
83
|
-
if (!existsSync(path))
|
|
84
|
-
return [];
|
|
85
|
-
try {
|
|
86
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
87
|
-
return Array.isArray(parsed) ? parsed.filter(isDroppedRewrite) : [];
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
58
|
+
return readList(droppedPath(root), isDropped);
|
|
92
59
|
}
|
|
60
|
+
/** Atomically replace the tombstone file. Writing `[]` clears it. */
|
|
93
61
|
export function writeDroppedRepairs(root, dropped) {
|
|
94
62
|
writeFileAtomic(droppedPath(root), JSON.stringify(dropped, null, 2) + "\n");
|
|
95
63
|
}
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
* `repair-provenance`'s own action is the one exception: it needs the RAW
|
|
103
|
-
* read (via readPendingRepairs) because it diffs raw-vs-swept to report what
|
|
104
|
-
* it cleaned up, and it persists the sweep back to disk. Every other
|
|
105
|
-
* reader — `hunch escalations`, SessionStart orientation, the `hunch_now`
|
|
106
|
-
* and `hunch_escalations` MCP tools — must call this instead, or a
|
|
107
|
-
* tombstoned entry that hasn't yet been swept by a `repair-provenance` run
|
|
108
|
-
* re-surfaces as if the human never answered it. */
|
|
64
|
+
/**
|
|
65
|
+
* The raw queue minus anything tombstoned. Read-only: it never changes either
|
|
66
|
+
* file. Every read-only consumer uses this -- `hunch escalations`, SessionStart
|
|
67
|
+
* orientation, and the `hunch_now` and `hunch_escalations` MCP tools. Only
|
|
68
|
+
* `repair-provenance` itself reads the raw queue.
|
|
69
|
+
*/
|
|
109
70
|
export function readActivePendingRepairs(root) {
|
|
110
71
|
return withoutDropped(readPendingRepairs(root), readDroppedRepairs(root));
|
|
111
72
|
}
|
|
112
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
* `--apply --only <id>` that's guaranteed to no-op forever. `null` from
|
|
119
|
-
* commitsExist (the check itself failed to run) is treated as fail-open —
|
|
120
|
-
* same discipline as the apply path — so nothing is misreported as
|
|
121
|
-
* withheld just because the check couldn't run.
|
|
122
|
-
*
|
|
123
|
-
* Returns the OBJECTS, not their ids, and every caller must pass the exact
|
|
124
|
-
* same `queued` array into both this function and whatever it hands the
|
|
125
|
-
* result to (commitRepairEscalations) — an id-keyed set would collapse a
|
|
126
|
-
* corrupted queue's two same-id entries (one resolvable, one not) into one
|
|
127
|
-
* unit, exactly the identity confusion withheldForUnresolvableTo's own
|
|
128
|
-
* docstring warns about. */
|
|
73
|
+
/**
|
|
74
|
+
* The queued entries whose proposed `to` does not resolve in this repository.
|
|
75
|
+
* Fails open (nothing withheld) when git cannot answer. The set holds the
|
|
76
|
+
* caller's own objects, so callers must pass the same `queued` array here and to
|
|
77
|
+
* whatever consumes the result.
|
|
78
|
+
*/
|
|
129
79
|
export function withheldRewrites(root, queued) {
|
|
130
80
|
const existing = commitsExist(queued.map((r) => r.to), root);
|
|
131
81
|
return new Set(withheldForUnresolvableTo(queued, existing).withheld);
|
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
|
@@ -307,32 +307,17 @@ export interface CommitCandidate {
|
|
|
307
307
|
sha: string;
|
|
308
308
|
files: string[];
|
|
309
309
|
}
|
|
310
|
-
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* set of now-orphaned source-branch commits. */
|
|
310
|
+
/** Commits in `oldRef..newRef` (most recent `maxCount`, oldest first), each with the
|
|
311
|
+
* non-deleted paths it touched. Squash-merge provenance repair reads this. A git
|
|
312
|
+
* failure yields []. */
|
|
314
313
|
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. */
|
|
314
|
+
/** Which of `shas` resolve to a commit, via one `git cat-file --batch-check`.
|
|
315
|
+
* Malformed values are dropped before the stdin lines are built (positional mapping).
|
|
316
|
+
* Returns null when git itself could not run: callers fail open. */
|
|
325
317
|
export declare function commitsExist(shas: readonly string[], cwd: string): Set<string> | null;
|
|
326
318
|
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. */
|
|
319
|
+
/** Whether `commit` is reachable from `ref`. Never collapses "no" and "error": only
|
|
320
|
+
* "orphaned" is repair-eligible. A missing or non-directory cwd is "unresolvable". */
|
|
336
321
|
export declare function commitRepairStatus(commit: string, ref: string, cwd: string): CommitRepairStatus;
|
|
337
322
|
/** commitRepairStatus against several tips: "orphaned" only when NO ref
|
|
338
323
|
* reaches the commit. A historical `--range old..new` must not make a commit
|