@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/cli/invocation.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ResolvedInvocation {
|
|
|
11
11
|
* or npx cache), never from a source checkout. */
|
|
12
12
|
installed: boolean;
|
|
13
13
|
}
|
|
14
|
+
/** Faint (dim) ANSI styling. */
|
|
14
15
|
export declare function dim(s: string): string;
|
|
15
16
|
/** Portable invocation written into committed MCP/provider configuration.
|
|
16
17
|
* Keep it independently testable so the distribution pin cannot silently
|
|
@@ -20,20 +21,14 @@ export declare function publishedMcpInvocation(): Invocation;
|
|
|
20
21
|
* PowerShell can execute the first token; paths and other unsafe tokens use
|
|
21
22
|
* JSON string quoting, which is accepted by PowerShell, cmd, and POSIX sh. */
|
|
22
23
|
export declare function shellInvocation(inv: Invocation): string;
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* assistant CLI found") shipped unnoticed through three review passes. That
|
|
28
|
-
* bug resurfaces here for the same reason: resolution.statuses carries a
|
|
29
|
-
* `subscription` field for the CLI providers but openai-compat's is null (it
|
|
30
|
-
* isn't a subscription), so it must be special-cased explicitly rather than
|
|
31
|
-
* falling through to the "no assistant CLI" branch. */
|
|
24
|
+
/** Render doctor's synthesis status lines. Exported for testing: an earlier,
|
|
25
|
+
* untested version reported openai-compat as "no assistant CLI found". Statuses
|
|
26
|
+
* carry a `subscription` for CLI providers but openai-compat's is null, so it is
|
|
27
|
+
* special-cased explicitly. */
|
|
32
28
|
export declare function synthesisStatusLines(resolution: ProviderResolution, env: NodeJS.ProcessEnv): string[];
|
|
33
|
-
/** Gate + fetch the Ollama context-window advisory
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* this one makes a best-effort network call. */
|
|
29
|
+
/** Gate + fetch the Ollama context-window advisory. Only openai-compat is
|
|
30
|
+
* relevant; kept apart from the sync status lines because it makes a
|
|
31
|
+
* best-effort network call. */
|
|
37
32
|
export declare function maybeWarnOllamaContext(providerName: string, env: NodeJS.ProcessEnv): Promise<string | null>;
|
|
38
33
|
/** Classify the entry path without faking import.meta.url in tests. */
|
|
39
34
|
export declare function classifyEntry(entry: string): {
|
package/dist/cli/invocation.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI logic that is free of import-time side effects, so any module
|
|
3
|
+
* (tests included) can import it safely, unlike src/cli/index.ts which runs the
|
|
4
|
+
* program as soon as it loads. It holds how to re-invoke this CLI from a git hook
|
|
5
|
+
* or `.mcp.json` (from a built dist and in dev via tsx), plus small formatting
|
|
6
|
+
* helpers (`dim()`, doctor's `synthesisStatusLines()`) that need the same import
|
|
7
|
+
* safety to be unit-testable.
|
|
8
|
+
*/
|
|
7
9
|
import { fileURLToPath } from "node:url";
|
|
8
10
|
import { probeOllamaNumCtx } from "../synthesis/provider.js";
|
|
9
11
|
import { HUNCH_NPX_PACKAGE_SPEC } from "../core/version.js";
|
|
12
|
+
/** Faint (dim) ANSI styling. */
|
|
10
13
|
export function dim(s) {
|
|
11
14
|
return `\x1b[2m${s}\x1b[0m`;
|
|
12
15
|
}
|
|
@@ -23,15 +26,10 @@ export function shellInvocation(inv) {
|
|
|
23
26
|
const token = (part) => /^[A-Za-z0-9_@:=+.,/-]+$/.test(part) ? part : JSON.stringify(part);
|
|
24
27
|
return [inv.command, ...inv.args].map(token).join(" ");
|
|
25
28
|
}
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* assistant CLI found") shipped unnoticed through three review passes. That
|
|
31
|
-
* bug resurfaces here for the same reason: resolution.statuses carries a
|
|
32
|
-
* `subscription` field for the CLI providers but openai-compat's is null (it
|
|
33
|
-
* isn't a subscription), so it must be special-cased explicitly rather than
|
|
34
|
-
* falling through to the "no assistant CLI" branch. */
|
|
29
|
+
/** Render doctor's synthesis status lines. Exported for testing: an earlier,
|
|
30
|
+
* untested version reported openai-compat as "no assistant CLI found". Statuses
|
|
31
|
+
* carry a `subscription` for CLI providers but openai-compat's is null, so it is
|
|
32
|
+
* special-cased explicitly. */
|
|
35
33
|
export function synthesisStatusLines(resolution, env) {
|
|
36
34
|
const provider = resolution.provider;
|
|
37
35
|
if (resolution.source === "unavailable-initiator") {
|
|
@@ -43,39 +41,38 @@ export function synthesisStatusLines(resolution, env) {
|
|
|
43
41
|
if (resolution.source === "initiator") {
|
|
44
42
|
return [` ↳ agent calls remain with ${resolution.initiator}, using that CLI's configured authentication.`];
|
|
45
43
|
}
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return [` ↳ LLM synthesis uses your ${
|
|
44
|
+
const status = resolution.statuses.find((s) => s.name === provider.name);
|
|
45
|
+
if (status?.subscription) {
|
|
46
|
+
return [` ↳ LLM synthesis uses your ${status.subscription}; provider API credentials are not used.`];
|
|
49
47
|
}
|
|
50
48
|
if (provider.name === "openai-compat") {
|
|
51
|
-
const base = env.HUNCH_SYNTH_BASE_URL
|
|
52
|
-
const model = env.HUNCH_SYNTH_MODEL
|
|
49
|
+
const base = env.HUNCH_SYNTH_BASE_URL || "(unset)";
|
|
50
|
+
const model = env.HUNCH_SYNTH_MODEL || "(unset)";
|
|
53
51
|
const keyNote = env.HUNCH_SYNTH_API_KEY ? " (HUNCH_SYNTH_API_KEY set)" : " (no API key)";
|
|
54
52
|
return [` ↳ LLM synthesis via local/self-hosted endpoint ${base} (model: ${model})${keyNote}`];
|
|
55
53
|
}
|
|
56
54
|
if (resolution.source === "ambiguous") {
|
|
57
|
-
const names = resolution.statuses.filter((s) => s.name !== "deterministic"
|
|
55
|
+
const names = resolution.statuses.filter((s) => s.available && s.name !== "deterministic").map((s) => s.name);
|
|
58
56
|
return [
|
|
59
57
|
dim(` ↳ ${names.join(", ")} are available; Hunch will not guess which provider to use.`),
|
|
60
|
-
dim(` choose one locally: ${names.map((
|
|
58
|
+
dim(` choose one locally: ${names.map((n) => `hunch provider ${n}`).join(" or ")}`),
|
|
61
59
|
];
|
|
62
60
|
}
|
|
63
61
|
if (resolution.source === "unavailable-preference") {
|
|
64
62
|
return [dim(` ↳ ${resolution.preference} was selected but is unavailable; using the offline heuristic.`)];
|
|
65
63
|
}
|
|
66
64
|
return [
|
|
67
|
-
dim(
|
|
68
|
-
dim(
|
|
65
|
+
dim(" ↳ no assistant CLI found — synthesis uses the offline heuristic (advisory, low-confidence)."),
|
|
66
|
+
dim(" install or log into Claude Code, Codex, or Cursor; then select one with `hunch provider <name>`."),
|
|
69
67
|
];
|
|
70
68
|
}
|
|
71
|
-
/** Gate + fetch the Ollama context-window advisory
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* this one makes a best-effort network call. */
|
|
69
|
+
/** Gate + fetch the Ollama context-window advisory. Only openai-compat is
|
|
70
|
+
* relevant; kept apart from the sync status lines because it makes a
|
|
71
|
+
* best-effort network call. */
|
|
75
72
|
export async function maybeWarnOllamaContext(providerName, env) {
|
|
76
73
|
if (providerName !== "openai-compat")
|
|
77
74
|
return null;
|
|
78
|
-
return probeOllamaNumCtx(env.HUNCH_SYNTH_BASE_URL
|
|
75
|
+
return probeOllamaNumCtx(env.HUNCH_SYNTH_BASE_URL || "", env.HUNCH_SYNTH_MODEL || "");
|
|
79
76
|
}
|
|
80
77
|
/** Classify the entry path without faking import.meta.url in tests. */
|
|
81
78
|
export function classifyEntry(entry) {
|
|
@@ -27,10 +27,10 @@ function view(file, source) {
|
|
|
27
27
|
}
|
|
28
28
|
function viewOfParsed(parsed) {
|
|
29
29
|
const symbols = new Map(parsed.symbols.map((s) => [symbolKey(s), s]));
|
|
30
|
-
const
|
|
30
|
+
const symbolAt = new Map(parsed.symbols.map((s, i) => [i, s]));
|
|
31
31
|
const calls = new Map();
|
|
32
32
|
for (const [index, callees] of attributeCalls(parsed)) {
|
|
33
|
-
const caller =
|
|
33
|
+
const caller = symbolAt.get(index);
|
|
34
34
|
if (!caller)
|
|
35
35
|
continue;
|
|
36
36
|
for (const [callee, member] of callees) {
|
|
@@ -93,21 +93,23 @@ function parsedSymbolFor(graphSymbol, parsed) {
|
|
|
93
93
|
const base = symbolId(graphSymbol.file, graphSymbol.name, graphSymbol.kind);
|
|
94
94
|
return matches.find((_symbol, index) => (index === 0 ? base : `${base}_${index}`) === graphSymbol.id) ?? null;
|
|
95
95
|
}
|
|
96
|
-
/** `start
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* so every scan and splice against
|
|
100
|
-
* based. */
|
|
101
|
-
function
|
|
96
|
+
/** Apply `{ start, end, text }` replacements to `source`. Offsets are JavaScript
|
|
97
|
+
* string (UTF-16 code unit) indices: the parser's `startByte`/`endByte`/`atByte`
|
|
98
|
+
* fields, despite their names, carry the same units (tree-sitter indexes the JS
|
|
99
|
+
* string it was given, not its UTF-8 bytes), so every scan and splice against
|
|
100
|
+
* them must be string-based, never Buffer/byte-based. */
|
|
101
|
+
function applyReplacements(source, replacements) {
|
|
102
102
|
let result = source;
|
|
103
103
|
for (const replacement of [...replacements].sort((a, b) => b.start - a.start)) {
|
|
104
104
|
result = result.slice(0, replacement.start) + replacement.text + result.slice(replacement.end);
|
|
105
105
|
}
|
|
106
106
|
return result;
|
|
107
107
|
}
|
|
108
|
-
/**
|
|
109
|
-
function
|
|
110
|
-
|
|
108
|
+
/** Offset where a new top-level import may go: 0, or just past a leading shebang line. */
|
|
109
|
+
function importInsertionOffset(source) {
|
|
110
|
+
if (!source.startsWith("#!"))
|
|
111
|
+
return 0;
|
|
112
|
+
return Math.max(0, source.indexOf("\n") + 1);
|
|
111
113
|
}
|
|
112
114
|
function mutateSource(policy, base, sourceFile, source) {
|
|
113
115
|
const assertion = policy.assertion;
|
|
@@ -150,10 +152,14 @@ function mutateSource(policy, base, sourceFile, source) {
|
|
|
150
152
|
const specifier = relativeSpecifier(sourceFile, targetFile);
|
|
151
153
|
if (parsed.imports.some((candidate) => candidate === specifier))
|
|
152
154
|
return { error: "mutation-component-import-already-present" };
|
|
153
|
-
const insertion =
|
|
155
|
+
const insertion = importInsertionOffset(source);
|
|
154
156
|
return {
|
|
155
157
|
file: sourceFile,
|
|
156
|
-
source:
|
|
158
|
+
source: applyReplacements(source, [{
|
|
159
|
+
start: insertion,
|
|
160
|
+
end: insertion,
|
|
161
|
+
text: `import ${JSON.stringify(specifier)}; // hunch deterministic component mutation\n`,
|
|
162
|
+
}]),
|
|
157
163
|
};
|
|
158
164
|
}
|
|
159
165
|
const subject = symbolForSelector(base, assertion.subject);
|
|
@@ -166,7 +172,7 @@ function mutateSource(policy, base, sourceFile, source) {
|
|
|
166
172
|
if (!definition)
|
|
167
173
|
return { error: "mutation-subject-definition-unresolved" };
|
|
168
174
|
if (assertion.kind === "exists") {
|
|
169
|
-
return { file: subject.file, source:
|
|
175
|
+
return { file: subject.file, source: applyReplacements(source, [{ start: definition.startByte, end: definition.endByte, text: "" }]) };
|
|
170
176
|
}
|
|
171
177
|
if (assertion.kind === "not-reaches"
|
|
172
178
|
&& assertion.relation.edges.length === 1
|
|
@@ -178,10 +184,14 @@ function mutateSource(policy, base, sourceFile, source) {
|
|
|
178
184
|
if (parsed.imports.some((specifier) => externalPackage(specifier) === dependency)) {
|
|
179
185
|
return { error: "mutation-forbidden-import-already-present" };
|
|
180
186
|
}
|
|
181
|
-
const insertion =
|
|
187
|
+
const insertion = importInsertionOffset(source);
|
|
182
188
|
return {
|
|
183
189
|
file: subject.file,
|
|
184
|
-
source:
|
|
190
|
+
source: applyReplacements(source, [{
|
|
191
|
+
start: insertion,
|
|
192
|
+
end: insertion,
|
|
193
|
+
text: `import ${JSON.stringify(dependency)}; // hunch deterministic source mutation\n`,
|
|
194
|
+
}]),
|
|
185
195
|
};
|
|
186
196
|
}
|
|
187
197
|
const object = symbolForSelector(base, assertion.object);
|
|
@@ -198,11 +208,11 @@ function mutateSource(policy, base, sourceFile, source) {
|
|
|
198
208
|
.map((call) => ({ start: call.atByte, end: call.endByte, text: "hunchMutationRemovedCall" }));
|
|
199
209
|
if (!replacements.length)
|
|
200
210
|
return { error: "mutation-required-call-unresolved" };
|
|
201
|
-
return { file: subject.file, source:
|
|
211
|
+
return { file: subject.file, source: applyReplacements(source, replacements) };
|
|
202
212
|
}
|
|
203
213
|
if (!assertion.relation.edges.includes("calls"))
|
|
204
214
|
return { error: "mutation-call-edge-not-supported" };
|
|
205
|
-
// String search
|
|
215
|
+
// String search: offsets are string indices (see applyReplacements), not bytes.
|
|
206
216
|
const open = source.indexOf("{", definition.startByte);
|
|
207
217
|
if (open < 0 || open >= definition.endByte)
|
|
208
218
|
return { error: "mutation-subject-body-unsupported" };
|
|
@@ -210,7 +220,7 @@ function mutateSource(policy, base, sourceFile, source) {
|
|
|
210
220
|
if (object.file !== subject.file) {
|
|
211
221
|
const specifier = relativeSpecifier(subject.file, object.file);
|
|
212
222
|
if (!parsed.imports.includes(specifier)) {
|
|
213
|
-
const insertion =
|
|
223
|
+
const insertion = importInsertionOffset(source);
|
|
214
224
|
replacements.push({
|
|
215
225
|
start: insertion,
|
|
216
226
|
end: insertion,
|
|
@@ -220,7 +230,7 @@ function mutateSource(policy, base, sourceFile, source) {
|
|
|
220
230
|
}
|
|
221
231
|
return {
|
|
222
232
|
file: subject.file,
|
|
223
|
-
source:
|
|
233
|
+
source: applyReplacements(source, replacements),
|
|
224
234
|
};
|
|
225
235
|
}
|
|
226
236
|
function removeWorktree(root, hooks, env, checkout) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Decision } from "./types.js";
|
|
2
2
|
import type { CommitCandidate, CommitRepairStatus } from "../extractors/git.js";
|
|
3
|
+
/** One proposed provenance fix: decision `id` cites `from`, which should cite `to`. */
|
|
3
4
|
export interface CommitRewrite {
|
|
4
5
|
id: string;
|
|
5
6
|
from: string;
|
|
@@ -8,14 +9,12 @@ export interface CommitRewrite {
|
|
|
8
9
|
/** Bind review to the ordered queue and which targets are currently resolvable.
|
|
9
10
|
* Queue order matters: duplicate decision ids use first-match selection. */
|
|
10
11
|
export declare function commitRepairReviewHash(queued: readonly CommitRewrite[], withheld?: ReadonlySet<CommitRewrite>): string;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
* {id, from} is a NEW proposal — not a repeat of the one that was rejected —
|
|
18
|
-
* and surfaces normally. */
|
|
12
|
+
/**
|
|
13
|
+
* Tombstone for a rewrite a human explicitly rejected with `--drop`. The key is
|
|
14
|
+
* the full `{id, from, to}` triple, not just `{id, from}`: a later merge that
|
|
15
|
+
* proposes a DIFFERENT `to` for the same `{id, from}` is a new proposal and
|
|
16
|
+
* must surface normally.
|
|
17
|
+
*/
|
|
19
18
|
export interface DroppedRewrite {
|
|
20
19
|
id: string;
|
|
21
20
|
from: string;
|
|
@@ -23,12 +22,10 @@ export interface DroppedRewrite {
|
|
|
23
22
|
}
|
|
24
23
|
export interface CommitRepairPlan {
|
|
25
24
|
rewrites: CommitRewrite[];
|
|
25
|
+
/** Distinct decision ids the rewrites touch. */
|
|
26
26
|
records: string[];
|
|
27
27
|
}
|
|
28
|
-
/** Live decisions whose cited commit
|
|
29
|
-
* ancestor of the current tip. Only "orphaned" qualifies: a commit that
|
|
30
|
-
* doesn't resolve at all ("unresolvable") is out of repair scope, and a git
|
|
31
|
-
* failure ("unknown") must never be mistaken for repair-eligible. */
|
|
28
|
+
/** Live decisions whose cited commit is orphaned (unresolvable/unknown/current are excluded). */
|
|
32
29
|
export declare function orphanedCommitDecisions(decisions: readonly Decision[], status: (sha: string) => CommitRepairStatus): Decision[];
|
|
33
30
|
/** Match each orphaned decision to exactly one candidate. A candidate with the
|
|
34
31
|
* same `git patch-id --stable` as the orphaned commit (a rebase- or
|
|
@@ -39,139 +36,47 @@ export declare function orphanedCommitDecisions(decisions: readonly Decision[],
|
|
|
39
36
|
* patch-id never equals any one of them, but weak: shared files like
|
|
40
37
|
* CHANGELOG.md often have one wrong superset (#450). */
|
|
41
38
|
export declare function planCommitRepair(orphaned: readonly Decision[], candidates: readonly CommitCandidate[], patchIdOf?: (sha: string) => string | null): CommitRepairPlan;
|
|
42
|
-
/**
|
|
43
|
-
* names still exists, still cites the commit the entry expects to replace (a
|
|
44
|
-
* decision that already moved on would make repairDecisionCommit's own
|
|
45
|
-
* stale-plan bail refuse it), and hasn't since been superseded/rejected —
|
|
46
|
-
* same exclusion as orphanedCommitDecisions, since repairing dead history's
|
|
47
|
-
* provenance isn't worth asking about. Backs the read-only escalation
|
|
48
|
-
* surface only (commitRepairEscalations) — absence-means-not-askable is
|
|
49
|
-
* safe there because it self-heals the moment visibility returns.
|
|
50
|
-
* repair-provenance itself does NOT use this: it reads the full store and
|
|
51
|
-
* may legitimately list (dry-run) or leave queued (apply) an entry this
|
|
52
|
-
* predicate would call not-live, so the escalation surface and the CLI can
|
|
53
|
-
* disagree about the same entry — see deadRewrites and resolvedRewriteIds
|
|
54
|
-
* for the CLI's own, looser-on-purpose reasoning. */
|
|
39
|
+
/** Queued entries that are still answerable: the decision exists, still cites `from`, and is live. */
|
|
55
40
|
export declare function liveRewrites(queued: readonly CommitRewrite[], decisions: readonly Decision[]): CommitRewrite[];
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
* docstring is why that matters — the queue is the one durable record of a
|
|
64
|
-
* match once ORIG_HEAD moves on and the matched-away commit can be gc'd, so
|
|
65
|
-
* deleting an entry on a merely-absent id would destroy it unrecoverably.
|
|
66
|
-
* Contrast liveRewrites, whose absence-means-not-askable rule is safe on
|
|
67
|
-
* the read-only escalation surface, which self-heals once visibility
|
|
68
|
-
* returns.
|
|
69
|
-
*
|
|
70
|
-
* Caveat: "demonstrably" is only as strong as this run's view of
|
|
71
|
-
* `decisions`, which comes from the same git-tracked, branch-dependent
|
|
72
|
-
* files that motivate treating ABSENCE as inconclusive. A decision whose
|
|
73
|
-
* `commit`/`status` genuinely differ between branches (e.g. a repair
|
|
74
|
-
* already applied and committed on one branch, checked out here from a
|
|
75
|
-
* branch that predates it) could still read as "moved on" when it hasn't,
|
|
76
|
-
* from this branch's perspective. Far narrower than the absence case this
|
|
77
|
-
* function exists to fix, and not addressed here. */
|
|
41
|
+
/**
|
|
42
|
+
* Queued entries whose decision IS visible but moved on (different or null
|
|
43
|
+
* commit) or was superseded/rejected. An id absent from `decisions` is never
|
|
44
|
+
* dead: absence comes from the reader's view (a branch checkout, an unmounted
|
|
45
|
+
* private overlay) and the CLI deletes whatever this returns. Caveat: a decision
|
|
46
|
+
* that differs between branches could still read as "moved on".
|
|
47
|
+
*/
|
|
78
48
|
export declare function deadRewrites(queued: readonly CommitRewrite[], decisions: readonly Decision[]): CommitRewrite[];
|
|
79
|
-
/**
|
|
80
|
-
* on), the ids repair-provenance may treat as RESOLVED once it's done: the
|
|
81
|
-
* decision is present in `decisions` this run, whether or not
|
|
82
|
-
* repairDecisionCommit actually changed it (a present decision that already
|
|
83
|
-
* moved on is "resolved" too — repairDecisionCommit's own bail refused it,
|
|
84
|
-
* and there is nothing more this run can do about it). An id whose decision
|
|
85
|
-
* is NOT present is never resolved — same reasoning as deadRewrites:
|
|
86
|
-
* absence is the reader's transient view, not proof the match is settled,
|
|
87
|
-
* so it must stay queued for a run where the decision is visible again. */
|
|
49
|
+
/** Ids of `toApply` whose decision is visible, whether or not the rewrite changed it. */
|
|
88
50
|
export declare function resolvedRewriteIds(toApply: readonly CommitRewrite[], decisions: readonly Decision[]): Set<string>;
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* match sharing {id, from} but proposing a different `to` is unaffected: it
|
|
93
|
-
* is a different proposal, not the rejected one.
|
|
94
|
-
*
|
|
95
|
-
* Preserves object identity for every surviving entry (a plain filter, never
|
|
96
|
-
* a clone or a rebuild) — repair-provenance's own action relies on this to
|
|
97
|
-
* diff its swept-vs-active queue by reference (`queue.filter(r =>
|
|
98
|
-
* !active.includes(r))`) rather than re-spelling the {id, from, to} key a
|
|
99
|
-
* third time. Do not change this to normalize or reconstruct entries
|
|
100
|
-
* without updating that call site too. */
|
|
101
|
-
export declare function withoutDropped(rewrites: readonly CommitRewrite[], dropped: readonly DroppedRewrite[]): CommitRewrite[];
|
|
102
|
-
/** Append newly-dropped {id, from, to} triples to the tombstone list,
|
|
103
|
-
* deduped — same append-only, dedupe-by-key shape as mergeRewrites. */
|
|
51
|
+
/** Drop rewrites whose exact {id, from, to} triple is tombstoned. A plain filter: survivors keep identity. */
|
|
52
|
+
export declare function withoutDropped<T extends CommitRewrite>(rewrites: readonly T[], dropped: readonly DroppedRewrite[]): T[];
|
|
53
|
+
/** `existing` followed by the entries of `newly` whose exact triple is not already tombstoned. */
|
|
104
54
|
export declare function addDropped(newly: readonly DroppedRewrite[], existing: readonly DroppedRewrite[]): DroppedRewrite[];
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
*
|
|
111
|
-
* Overrides by object identity, targeting exactly the one queued entry
|
|
112
|
-
* firstFor (id) would pick — the same first-match rule --drop applies, and
|
|
113
|
-
* the same identity-not-id targeting the dead-entry prune applies (#53; the
|
|
114
|
-
* prune itself removes every provably-dead entry, not just a first match).
|
|
115
|
-
* A corrupted queue file, a hand edit, or a bug upstream could otherwise
|
|
116
|
-
* carry a SECOND entry sharing that id; overriding by id alone would
|
|
117
|
-
* silently destroy that still-queued sibling too, with no tombstone and no
|
|
118
|
-
* way to recover it, even though it was never the entry that would have
|
|
119
|
-
* been applied. Any such sibling is left queued and untouched.
|
|
120
|
-
*
|
|
121
|
-
* How a survivor resolves: the dead-entry prune (deadRewrites), once
|
|
122
|
-
* --apply has moved the decision past the sibling's `from`; or a --drop
|
|
123
|
-
* whose tombstone matches the sibling's exact {id, from, to} — the same
|
|
124
|
-
* --drop that dropped the fresh entry, when the sibling happens to share
|
|
125
|
-
* its triple (the drop's own re-sweep, withoutDropped, catches it too), a
|
|
126
|
-
* second explicit --drop otherwise. NOT a later fresh detection: by this
|
|
127
|
-
* same first-match rule it always overrides the fresh entry sitting ahead
|
|
128
|
-
* of it, never the sibling. */
|
|
55
|
+
/**
|
|
56
|
+
* Fresh detections first, then the queued entries they did not override. A fresh
|
|
57
|
+
* entry overrides only the FIRST queued entry with its id (see `firstFor`);
|
|
58
|
+
* later duplicate-id siblings survive untouched.
|
|
59
|
+
*/
|
|
129
60
|
export declare function mergeRewrites(fresh: readonly CommitRewrite[], queued: readonly CommitRewrite[]): CommitRewrite[];
|
|
130
|
-
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*
|
|
137
|
-
* A corrupted queue file, a hand-edited entry, or a bug upstream could
|
|
138
|
-
* otherwise get a `to` that never resolves to a real commit written
|
|
139
|
-
* straight into a decision's `commit` field and provenance evidence, then
|
|
140
|
-
* auto-committed into shared team memory. `withheld` entries must stay
|
|
141
|
-
* queued (never deleted, never passed to repairDecisionCommit) — the queue
|
|
142
|
-
* file is the one durable record of the match, same reasoning as
|
|
143
|
-
* deadRewrites/resolvedRewriteIds.
|
|
144
|
-
*
|
|
145
|
-
* Both returned arrays preserve object identity (never clones or rebuilds
|
|
146
|
-
* an entry) — the CLI's queue sweep and its "still queued" reporting both
|
|
147
|
-
* key a `Set` off the exact objects in `withheld`, by reference, precisely
|
|
148
|
-
* because a corrupted queue file can carry two entries sharing an id (one
|
|
149
|
-
* resolvable, one not): an id alone can't tell them apart. Do not change
|
|
150
|
-
* this to normalize or reconstruct entries without updating those call
|
|
151
|
-
* sites too — same caveat withoutDropped's own docstring carries. */
|
|
152
|
-
export declare function withheldForUnresolvableTo(toApply: readonly CommitRewrite[], existing: Set<string> | null): {
|
|
61
|
+
/**
|
|
62
|
+
* Split entries by whether their proposed `to` resolves here. `existing === null`
|
|
63
|
+
* means the check could not run, so it fails open (everything applicable).
|
|
64
|
+
* Order and object references are preserved.
|
|
65
|
+
*/
|
|
66
|
+
export declare function withheldForUnresolvableTo(toApply: readonly CommitRewrite[], existing: ReadonlySet<string> | null): {
|
|
153
67
|
applicable: CommitRewrite[];
|
|
154
68
|
withheld: CommitRewrite[];
|
|
155
69
|
};
|
|
156
|
-
/** The
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
* withheldForUnresolvableTo's, and mergeRewrites' own docstrings). */
|
|
162
|
-
export declare function firstFor(entries: readonly CommitRewrite[], id: string): CommitRewrite | undefined;
|
|
163
|
-
/** pickRewrite is firstFor scoped to a plan's own rewrites (planCommitRepair
|
|
164
|
-
* itself never produces same-id duplicates, but a plan carrying them isn't
|
|
165
|
-
* ruled out here — see firstFor). repairDecisionCommit uses this to decide
|
|
166
|
-
* what to WRITE; a caller that needs to know which entry WOULD be picked
|
|
167
|
-
* without writing anything (the CLI's --apply summary and queue sweep)
|
|
168
|
-
* calls this same function, so the two can never independently drift on
|
|
169
|
-
* which entry "won". */
|
|
70
|
+
/** The single first-match rule for duplicate decision ids. */
|
|
71
|
+
export declare function firstFor<T extends {
|
|
72
|
+
id: string;
|
|
73
|
+
}>(entries: readonly T[], id: string): T | undefined;
|
|
74
|
+
/** The plan's rewrite for `id` (first match). */
|
|
170
75
|
export declare function pickRewrite(plan: CommitRepairPlan, id: string): CommitRewrite | undefined;
|
|
171
|
-
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Pure: apply the plan's rewrite to a decision. Returns the same reference when
|
|
78
|
+
* there is no entry or the record moved on (`commit` no longer equals `from`);
|
|
79
|
+
* otherwise a copy with `commit` and the `commit:<from>` evidence moved to `to`
|
|
80
|
+
* (deduped if `to` was already cited).
|
|
81
|
+
*/
|
|
177
82
|
export declare function repairDecisionCommit(d: Decision, plan: CommitRepairPlan): Decision;
|