@davesheffer/hunch 1.41.4 → 1.41.6
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/core/docanchors.js +2 -1
- package/dist/core/eol.d.ts +10 -0
- package/dist/core/eol.js +23 -0
- package/dist/core/groundingMerge.js +8 -6
- package/dist/extractors/k8sManifest.js +15 -42
- package/dist/integrations/gitignore.js +2 -1
- package/dist/integrations/hooks.js +5 -4
- package/package.json +4 -4
- package/server.json +2 -2
package/dist/core/docanchors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { currentForTopic, rejectedForTopic } from "./topics.js";
|
|
2
|
+
import { normalizeEol } from "./eol.js";
|
|
2
3
|
const MARKER = /<!--\s*hunch:topic\s+([A-Za-z0-9._/-]+)(?:\s+(dec_[A-Za-z0-9]+))?\s*-->/g;
|
|
3
4
|
/** Expand the leading whitespace of a line to columns. A tab advances to the
|
|
4
5
|
* NEXT 4-column tab stop (CommonMark), not a flat 4 columns: after two
|
|
@@ -238,7 +239,7 @@ export function parseDocAnchors(text) {
|
|
|
238
239
|
// a live, pinned anchor. Normalizing both CRLF and lone CR once here keeps
|
|
239
240
|
// fencedRanges/inlineSpanRanges/MARKER offsets consistent with each other
|
|
240
241
|
// and with the line numbers reported below.
|
|
241
|
-
text = text
|
|
242
|
+
text = normalizeEol(text);
|
|
242
243
|
const out = [];
|
|
243
244
|
const skip = [...fencedRanges(text), ...inlineSpanRanges(text)];
|
|
244
245
|
MARKER.lastIndex = 0;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Canonical line endings for parsers that do not expose source offsets. */
|
|
2
|
+
export declare function normalizeEol(text: string): string;
|
|
3
|
+
/** Remove the CR retained by split("\n") without changing source offsets. */
|
|
4
|
+
export declare function lineContent(line: string): string;
|
|
5
|
+
/** Read logical lines while keeping offsets in the original UTF-16 source. */
|
|
6
|
+
export declare function sourceLines(text: string): Generator<{
|
|
7
|
+
content: string;
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
}>;
|
package/dist/core/eol.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Canonical line endings for parsers that do not expose source offsets. */
|
|
2
|
+
export function normalizeEol(text) {
|
|
3
|
+
return text.replace(/\r\n?|\n/g, "\n");
|
|
4
|
+
}
|
|
5
|
+
/** Remove the CR retained by split("\n") without changing source offsets. */
|
|
6
|
+
export function lineContent(line) {
|
|
7
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
8
|
+
}
|
|
9
|
+
/** Read logical lines while keeping offsets in the original UTF-16 source. */
|
|
10
|
+
export function* sourceLines(text) {
|
|
11
|
+
let start = 0;
|
|
12
|
+
for (let i = 0; i < text.length; i++) {
|
|
13
|
+
if (text[i] !== "\r" && text[i] !== "\n")
|
|
14
|
+
continue;
|
|
15
|
+
const end = i;
|
|
16
|
+
if (text[i] === "\r" && text[i + 1] === "\n")
|
|
17
|
+
i++;
|
|
18
|
+
yield { content: text.slice(start, end), start, end };
|
|
19
|
+
start = i + 1;
|
|
20
|
+
}
|
|
21
|
+
yield { content: text.slice(start), start, end: text.length };
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=eol.js.map
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
22
|
import { parseGroundingCounts, stripCountsMatch, renderCountsMatch } from "./groundingLag.js";
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
import { normalizeEol } from "./eol.js";
|
|
24
|
+
// The entry point normalizes EOL before matching. `^...$` with /m anchors
|
|
25
|
+
// each marker at its own line start rather than
|
|
26
26
|
// requiring a specific preceding/following literal newline, so a hunk whose
|
|
27
27
|
// ours or theirs side is EMPTY (one side deleted the line, the other edited
|
|
28
28
|
// it) still matches — with a literal `\n` requirement there, git's real
|
|
29
29
|
// output for that shape has no such newline to match, so the whole hunk
|
|
30
30
|
// silently fails to match at all.
|
|
31
|
-
const CONFLICT_RE = /^<<<<<<< ours\
|
|
31
|
+
const CONFLICT_RE = /^<<<<<<< ours\n([\s\S]*?)^\|\|\|\|\|\|\| base\n[\s\S]*?^=======\n([\s\S]*?)^>>>>>>> theirs[^\n]*\n?/gm;
|
|
32
32
|
/** `null` when the hunk isn't confined to the counts sentence; otherwise the
|
|
33
33
|
* resolved sentence text, with each field taken as max(ours, theirs). */
|
|
34
34
|
function resolveCountsOnlyHunk(ours, theirs) {
|
|
@@ -52,8 +52,10 @@ function resolveCountsOnlyHunk(ours, theirs) {
|
|
|
52
52
|
export function resolveGroundingConflicts(diff3Text) {
|
|
53
53
|
if (!diff3Text.includes("<<<<<<< ours"))
|
|
54
54
|
return { conflict: false, text: diff3Text };
|
|
55
|
+
const eol = diff3Text.includes("\r\n") ? "\r\n" : diff3Text.includes("\r") ? "\r" : "\n";
|
|
56
|
+
const normalized = normalizeEol(diff3Text);
|
|
55
57
|
let allResolved = true;
|
|
56
|
-
const resolved =
|
|
58
|
+
const resolved = normalized.replace(CONFLICT_RE, (whole, ours, theirs) => {
|
|
57
59
|
const merged = resolveCountsOnlyHunk(ours, theirs);
|
|
58
60
|
if (merged === null) {
|
|
59
61
|
allResolved = false;
|
|
@@ -66,7 +68,7 @@ export function resolveGroundingConflicts(diff3Text) {
|
|
|
66
68
|
// marker survives, this is a real conflict, full stop.
|
|
67
69
|
if (!allResolved || resolved.includes("<<<<<<< ours"))
|
|
68
70
|
return { conflict: true, text: diff3Text };
|
|
69
|
-
return { conflict: false, text: resolved };
|
|
71
|
+
return { conflict: false, text: eol === "\n" ? resolved : resolved.replace(/\n/g, eol) };
|
|
70
72
|
}
|
|
71
73
|
/** Run `git merge-file --diff3` on real files and resolve the result.
|
|
72
74
|
* `write: null` means git itself errored (e.g. one side is binary) — never
|
|
@@ -1,25 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Deterministic text scan for Kubernetes manifest cross-resource references —
|
|
3
|
-
* NOT a tree-sitter walk. Confirmed directly (not assumed): parsing a realistic
|
|
4
|
-
* templated Deployment with this repo's own tree-sitter-yaml bundle showed a
|
|
5
|
-
* same-line templated scalar (`name: {{ include "x" . }}`) parses cleanly, but
|
|
6
|
-
* a block-form injection on its own line (`labels:\n {{- include ... }}` — the
|
|
7
|
-
* pattern real charts use for metadata.labels and spec.selector) collapses the
|
|
8
|
-
* WHOLE REST OF THE DOCUMENT'S error recovery into a flat, unstructured ERROR
|
|
9
|
-
* node. Real charts place such an injection early (right after metadata.name),
|
|
10
|
-
* so it would poison parsing for every field-path this module needs further
|
|
11
|
-
* down the same document. parseSource() (parse.ts) also never exposes its
|
|
12
|
-
* tree-sitter tree to callers at all -- helm.ts's own text-scan precedent
|
|
13
|
-
* exists for the same reason.
|
|
14
|
-
*
|
|
15
|
-
* This is a line-oriented, indentation-tracking scanner (not flat regex, unlike
|
|
16
|
-
* helm.ts's `{{ }}`-action scan) because K8s field-paths are nested block
|
|
17
|
-
* structure (`spec.template.spec.containers[].env[].valueFrom.secretKeyRef.name`)
|
|
18
|
-
* that a flat scan can't reconstruct. It never interprets `{{ }}` as YAML syntax
|
|
19
|
-
* at all -- a `{{ ... }}` on a line's value side is just that line's raw value
|
|
20
|
-
* text, same-line or block-form alike -- so it is immune to the exact failure
|
|
21
|
-
* mode that broke tree-sitter.
|
|
22
|
-
*/
|
|
1
|
+
import { sourceLines } from "../core/eol.js";
|
|
23
2
|
// ReplicaSet is included alongside the pod-spec-embedding/networking kinds
|
|
24
3
|
// specifically because it's the dominant real-world bearer of
|
|
25
4
|
// ownerReferences (Deployment -> ReplicaSet -> Pod is the common chain) --
|
|
@@ -42,14 +21,9 @@ function wildcardPath(path) {
|
|
|
42
21
|
function framePath(frames) {
|
|
43
22
|
return frames.map((f) => f.key).filter(Boolean).join(".").replace(/\.\[/g, "[");
|
|
44
23
|
}
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
// matches at the true end of the string. That silently zeroes out this whole
|
|
49
|
-
// module's output on any Windows clone, with no error. `.*?` (lazy, not `.*`
|
|
50
|
-
// greedy) so `\r?` gets first claim on a trailing `\r` instead of the value
|
|
51
|
-
// capture swallowing it.
|
|
52
|
-
const KEY_LINE = /^(\s*)(-\s+)?([A-Za-z0-9_.\/-]+):[ \t]*(.*?)\r?$/;
|
|
24
|
+
// sourceLines removes line terminators while retaining offsets in the original
|
|
25
|
+
// source, so every line parser sees the same content on LF, CRLF, and CR input.
|
|
26
|
+
const KEY_LINE = /^(\s*)(-\s+)?([A-Za-z0-9_.\/-]+):[ \t]*(.*?)$/;
|
|
53
27
|
// A line that is ENTIRELY a `{{ ... }}` template action (no `key:` prefix at
|
|
54
28
|
// all) -- e.g. a block-form injection appearing as a SIBLING after other
|
|
55
29
|
// literal keys under the same mapping (`app: my-app` then, on its own later
|
|
@@ -70,7 +44,7 @@ const BARE_TEMPLATE_LINE = /^\s*(?:-\s+)?\{\{[\s\S]*$/;
|
|
|
70
44
|
// branch, which used ORDINARY popping and lost the list-item frame entirely
|
|
71
45
|
// -- reparenting the item's real children one level up (`containers.name`
|
|
72
46
|
// instead of `containers[0].name`), silently dropping every reference under it.
|
|
73
|
-
const BARE_LIST_MARKER = /^(\s*)-[ \t]
|
|
47
|
+
const BARE_LIST_MARKER = /^(\s*)-[ \t]*$/;
|
|
74
48
|
// A list item whose ENTIRE content is a block-scalar header (`- |`, `- |-`,
|
|
75
49
|
// `- >`, `- |2`, `- | # comment`) -- the idiomatic way a chart inlines a shell
|
|
76
50
|
// script into `args:`/`command:`. The item's value is opaque TEXT, not YAML,
|
|
@@ -84,7 +58,7 @@ const BARE_LIST_MARKER = /^(\s*)-[ \t]*\r?$/;
|
|
|
84
58
|
// match. The rest is captured (not `[|>]` inline) so the header can be run
|
|
85
59
|
// through stripTrailingComment before BLOCK_SCALAR_HEADER, exactly as a
|
|
86
60
|
// key line's value is.
|
|
87
|
-
const BLOCK_SCALAR_LIST_ITEM = /^(\s*)-[ \t]+(.*?)
|
|
61
|
+
const BLOCK_SCALAR_LIST_ITEM = /^(\s*)-[ \t]+(.*?)$/;
|
|
88
62
|
// A YAML block-scalar header (`|`, `>`, plus an optional chomping indicator
|
|
89
63
|
// `+`/`-` and/or an explicit indent digit, in either order: `|`, `|-`, `>+`,
|
|
90
64
|
// `|2`, `|2-`, `|-2`). When a key's value is JUST this header, the real
|
|
@@ -161,7 +135,6 @@ function scanFieldPaths(text, baseChar) {
|
|
|
161
135
|
// (selector/labels) cares, and it checks this set itself.
|
|
162
136
|
const valuelessKeyParents = new Set();
|
|
163
137
|
const stack = [];
|
|
164
|
-
let charOffset = baseChar;
|
|
165
138
|
// Indent of the `-` of a `- |` list item whose block-scalar body is being
|
|
166
139
|
// skipped, or null when no such skip is active. The body ends at the first
|
|
167
140
|
// line indented at or shallower than that dash (YAML's own rule for where a
|
|
@@ -229,13 +202,11 @@ function scanFieldPaths(text, baseChar) {
|
|
|
229
202
|
unresolvedContainers.add(framePath(stack.slice(0, depth)));
|
|
230
203
|
}
|
|
231
204
|
};
|
|
232
|
-
for (const line of text
|
|
233
|
-
const lineStartChar =
|
|
234
|
-
charOffset += line.length + 1; // +1 for the \n split() consumed
|
|
205
|
+
for (const { content: line, start } of sourceLines(text)) {
|
|
206
|
+
const lineStartChar = baseChar + start;
|
|
235
207
|
// Inside a `- |` item's body: these lines are script/config TEXT, so they
|
|
236
|
-
// must never be read as field structure.
|
|
237
|
-
//
|
|
238
|
-
// every atChar/endChar after the block scalar depends on it.
|
|
208
|
+
// must never be read as field structure. sourceLines retains original
|
|
209
|
+
// offsets even when skipped lines use CRLF.
|
|
239
210
|
if (blockScalarSkipIndent !== null) {
|
|
240
211
|
// A blank line is part of the block scalar (YAML lets a scalar body
|
|
241
212
|
// contain empty lines at any indent), never its terminator.
|
|
@@ -671,12 +642,14 @@ function buildDocument(text, docStartChar, entries, unresolvedContainers, valuel
|
|
|
671
642
|
// deliberately NOT a separator -- real YAML doesn't treat it as one either,
|
|
672
643
|
// and it still isn't here: the dashes must be followed by whitespace or end
|
|
673
644
|
// of line.
|
|
674
|
-
const DOC_SEPARATOR = /^(?:---(?:[ \t]+.*)?|\.\.\.)[ \t]
|
|
645
|
+
const DOC_SEPARATOR = /^(?:---(?:[ \t]+.*)?|\.\.\.)[ \t]*$/;
|
|
675
646
|
export function extractK8sManifest(source) {
|
|
676
647
|
const docs = [];
|
|
677
648
|
const boundaries = [];
|
|
678
|
-
for (const
|
|
679
|
-
|
|
649
|
+
for (const line of sourceLines(source)) {
|
|
650
|
+
if (DOC_SEPARATOR.test(line.content))
|
|
651
|
+
boundaries.push(line.start, line.end);
|
|
652
|
+
}
|
|
680
653
|
const starts = [0, ...boundaries.filter((_, i) => i % 2 === 1)];
|
|
681
654
|
const ends = [...boundaries.filter((_, i) => i % 2 === 0), source.length];
|
|
682
655
|
for (let i = 0; i < starts.length; i++) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { readFileSync, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
16
16
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
17
17
|
import { writeFileAtomic } from "../core/io.js";
|
|
18
|
+
import { lineContent } from "../core/eol.js";
|
|
18
19
|
import { gitTrackedPaths, gitUntrackCached, isGitRepoRoot } from "../extractors/git.js";
|
|
19
20
|
const MARK = "# >>> hunch (derived runtime index — regenerable from .hunch/*.json) >>>";
|
|
20
21
|
const END = "# <<< hunch <<<";
|
|
@@ -116,7 +117,7 @@ export function assertSafeTopLevelConfigFile(root, name) {
|
|
|
116
117
|
}
|
|
117
118
|
return path;
|
|
118
119
|
}
|
|
119
|
-
const cleanLine = (line) => line
|
|
120
|
+
const cleanLine = (line) => lineContent(line).trim();
|
|
120
121
|
/** Locate a managed block as whole lines: the marker line and the FIRST end-marker
|
|
121
122
|
* line after it. `null` when either is missing — a hand-damaged block is never
|
|
122
123
|
* guessed at (con_8460b6770f). Indices are into `text.split("\n")`, whose items
|
|
@@ -20,6 +20,7 @@ import { homedir } from "node:os";
|
|
|
20
20
|
import { hooksDir, gitCommonDir, isLinkedWorktree, mainWorktreeRoot } from "../extractors/git.js";
|
|
21
21
|
import { initiatorChildEnv } from "../synthesis/initiator.js";
|
|
22
22
|
import { HUNCH_NPX_PACKAGE_SPEC, HUNCH_PACKAGE_NAME } from "../core/version.js";
|
|
23
|
+
import { normalizeEol } from "../core/eol.js";
|
|
23
24
|
const MARK = "# >>> hunch post-commit >>>";
|
|
24
25
|
const ENDMARK = "# <<< hunch post-commit <<<";
|
|
25
26
|
const GIT_CONTEXT = { checkoutType: "$3", portable: false };
|
|
@@ -73,7 +74,7 @@ const BLOCK_CLOSE = /^(?:fi|esac|done)\b|^\}|[;\s](?:fi|esac|done|\})\s*;?\s*$/;
|
|
|
73
74
|
* or a trailing top-level `if … else … fi` whose every branch ends in
|
|
74
75
|
* `exec`/`exit` — the shape of the pre-commit framework's generated hook. */
|
|
75
76
|
export function terminalExitLine(content) {
|
|
76
|
-
const lines = content
|
|
77
|
+
const lines = normalizeEol(content).split("\n");
|
|
77
78
|
let depth = 0;
|
|
78
79
|
for (const raw of lines) {
|
|
79
80
|
const line = raw.trim();
|
|
@@ -386,7 +387,7 @@ function blockFlags(line) {
|
|
|
386
387
|
function builtFlags(blk, mark, liveRoot) {
|
|
387
388
|
const { re } = blockSubcommand(mark);
|
|
388
389
|
const found = new Set();
|
|
389
|
-
for (const line of blk
|
|
390
|
+
for (const line of normalizeEol(blk).split("\n")) {
|
|
390
391
|
if (line.trim().startsWith("#"))
|
|
391
392
|
continue;
|
|
392
393
|
const m = re.exec(line);
|
|
@@ -417,7 +418,7 @@ function inspectBlock(file, mark, root) {
|
|
|
417
418
|
const { re, words } = blockSubcommand(mark);
|
|
418
419
|
const text = readText(file) ?? "";
|
|
419
420
|
const at = text.indexOf(mark);
|
|
420
|
-
const body = at < 0 ? [] : text.slice(at)
|
|
421
|
+
const body = at < 0 ? [] : normalizeEol(text.slice(at)).split("\n").slice(1);
|
|
421
422
|
let first = null;
|
|
422
423
|
for (const line of body) {
|
|
423
424
|
if (/^# <<< hunch /.test(line))
|
|
@@ -511,7 +512,7 @@ function worktreeTops(root) {
|
|
|
511
512
|
const out = gitRun(["worktree", "list", "--porcelain"], root).stdout;
|
|
512
513
|
if (!out)
|
|
513
514
|
return { linked: [], count: 0 };
|
|
514
|
-
const entries = out
|
|
515
|
+
const entries = normalizeEol(out).split(/\n\s*\n/).map((e) => e.split("\n"));
|
|
515
516
|
const parsed = entries
|
|
516
517
|
.map((lines) => ({
|
|
517
518
|
path: lines.find((l) => l.startsWith("worktree "))?.slice("worktree ".length) ?? "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.6",
|
|
4
4
|
"mcpName": "io.github.davesheffer/hunch",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
"build": "npm run clean && tsc -p tsconfig.json",
|
|
82
82
|
"dev": "tsx src/cli/index.ts",
|
|
83
83
|
"hunch": "tsx src/cli/index.ts",
|
|
84
|
-
"test": "
|
|
85
|
-
"test:e2e:md1": "
|
|
86
|
-
"test:e2e:team-matrix": "
|
|
84
|
+
"test": "node tooling/run-tests.mjs --test-concurrency=4 test/*.test.ts",
|
|
85
|
+
"test:e2e:md1": "node tooling/run-tests.mjs test/md1-e2e.test.ts",
|
|
86
|
+
"test:e2e:team-matrix": "node tooling/run-tests.mjs test/team-matrix-e2e.test.ts",
|
|
87
87
|
"verify:matrix": "node tooling/matrix-release-verification.mjs",
|
|
88
88
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
89
89
|
"rehearse:constitution": "npm run build && node tooling/constitution-clean-rehearsal.mjs",
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://www.hunchmemory.com",
|
|
10
|
-
"version": "1.41.
|
|
10
|
+
"version": "1.41.6",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.41.
|
|
16
|
+
"version": "1.41.6",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|