@davesheffer/hunch 1.8.2 → 1.9.2
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/README.md +96 -1
- package/dist/cli/index.js +1238 -396
- package/dist/constitution/adapters.js +31 -14
- package/dist/constitution/behaviorEvaluator.js +20 -7
- package/dist/constitution/behaviorProof.js +3 -2
- package/dist/constitution/canonical.js +7 -1
- package/dist/constitution/card.js +7 -2
- package/dist/constitution/compiler.js +71 -1
- package/dist/constitution/correctionPolicyMaterializer.js +496 -0
- package/dist/constitution/delta.js +3 -2
- package/dist/constitution/evaluator.js +29 -3
- package/dist/constitution/experiment.js +96 -5
- package/dist/constitution/experimentRunner.js +43 -14
- package/dist/constitution/g2BehaviorCandidates.js +49 -26
- package/dist/constitution/g2BehaviorDependencies.js +203 -14
- package/dist/constitution/g2Candidates.js +1 -1
- package/dist/constitution/lifecycle.js +17 -0
- package/dist/constitution/plan.js +26 -9
- package/dist/constitution/replacementFreeGit.js +67 -0
- package/dist/constitution/replay.js +6 -0
- package/dist/constitution/replayCache.js +1 -1
- package/dist/constitution/replayWorker.js +1 -1
- package/dist/constitution/repository.js +141 -5
- package/dist/constitution/safeCheckout.js +75 -0
- package/dist/constitution/schema.js +30 -5
- package/dist/constitution/service.js +74 -14
- package/dist/constitution/sourceMutation.js +65 -12
- package/dist/constitution/staticGraphBaseline.js +44 -0
- package/dist/constitution/structural.js +60 -4
- package/dist/core/autoreview.js +1 -1
- package/dist/core/canonicalOrder.js +6 -0
- package/dist/core/conformance.js +68 -27
- package/dist/core/docscan.js +2 -1
- package/dist/core/escalations.js +11 -0
- package/dist/core/io.js +44 -9
- package/dist/core/overlaySafety.js +178 -0
- package/dist/core/paths.js +13 -2
- package/dist/core/safeRepoFile.js +74 -0
- package/dist/extractors/comments.js +6 -8
- package/dist/extractors/git.js +1631 -82
- package/dist/extractors/indexer.js +86 -47
- package/dist/extractors/repoSource.js +390 -0
- package/dist/integrations/ciAction.js +10 -2
- package/dist/integrations/gitignore.js +44 -5
- package/dist/integrations/mergeDriver.js +23 -5
- package/dist/integrations/sync.js +61 -5
- package/dist/integrations/team.js +666 -23
- package/dist/mcp/server.js +261 -34
- package/dist/store/db.js +57 -7
- package/dist/store/hunchStore.js +92 -11
- package/dist/store/jsonStore.js +350 -63
- package/dist/store/schema.js +27 -11
- package/dist/synthesis/provider.js +13 -4
- package/dist/synthesis/synthesize.js +56 -19
- package/dist/wiki/graph.js +5 -4
- package/dist/wiki/wiki.js +16 -10
- package/package.json +15 -3
- package/tooling/competitive-watch.mjs +108 -0
- package/tooling/md1-benchmark.mjs +628 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
/** Automatic repository scans are a convenience boundary, not a license to
|
|
4
|
+
* materialize arbitrarily large tracked blobs in memory. Eight MiB is well
|
|
5
|
+
* above ordinary source-file sizes while keeping one malicious/generated file
|
|
6
|
+
* from becoming an unbounded descriptor read. */
|
|
7
|
+
export const MAX_REPO_SOURCE_FILE_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
/** Build a no-follow reader for automatic repository scanners. A Git-tracked
|
|
9
|
+
* symlink still appears in `git ls-files`; following it can copy host data into
|
|
10
|
+
* generated memory or reports. This reader accepts only one unchanged regular
|
|
11
|
+
* file beneath the canonical repository root and reads through the descriptor
|
|
12
|
+
* whose identity was checked. */
|
|
13
|
+
export function createRepoFileBufferReader(root, options = {}) {
|
|
14
|
+
const lexicalRoot = resolve(root);
|
|
15
|
+
const canonicalRoot = realpathSync(lexicalRoot);
|
|
16
|
+
const maxBytes = options.maxBytes ?? MAX_REPO_SOURCE_FILE_BYTES;
|
|
17
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
18
|
+
throw new RangeError("maxBytes must be a non-negative safe integer");
|
|
19
|
+
}
|
|
20
|
+
return (file) => {
|
|
21
|
+
let descriptor;
|
|
22
|
+
try {
|
|
23
|
+
const target = isAbsolute(file) ? resolve(file) : resolve(lexicalRoot, file);
|
|
24
|
+
const lexicalRelative = relative(lexicalRoot, target);
|
|
25
|
+
if (!lexicalRelative || lexicalRelative === ".." || lexicalRelative.startsWith(`..${sep}`) || isAbsolute(lexicalRelative))
|
|
26
|
+
return null;
|
|
27
|
+
const before = lstatSync(target);
|
|
28
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
29
|
+
return null;
|
|
30
|
+
const canonicalTarget = realpathSync(target);
|
|
31
|
+
const canonicalRelative = relative(canonicalRoot, canonicalTarget);
|
|
32
|
+
if (!canonicalRelative || canonicalRelative === ".." || canonicalRelative.startsWith(`..${sep}`) || isAbsolute(canonicalRelative)
|
|
33
|
+
|| canonicalTarget !== resolve(canonicalRoot, lexicalRelative)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
descriptor = openSync(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
37
|
+
const opened = fstatSync(descriptor);
|
|
38
|
+
const after = lstatSync(target);
|
|
39
|
+
if (!opened.isFile() || !after.isFile() || after.isSymbolicLink()
|
|
40
|
+
|| opened.size > maxBytes
|
|
41
|
+
|| opened.dev !== before.dev || opened.ino !== before.ino
|
|
42
|
+
|| after.dev !== opened.dev || after.ino !== opened.ino
|
|
43
|
+
|| realpathSync(target) !== canonicalTarget) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// Read exactly the size that passed fstat. readFileSync(fd) reads to EOF,
|
|
47
|
+
// so an in-place grow after the check could otherwise defeat the ceiling.
|
|
48
|
+
const bytes = Buffer.allocUnsafe(opened.size);
|
|
49
|
+
let offset = 0;
|
|
50
|
+
while (offset < bytes.length) {
|
|
51
|
+
const read = readSync(descriptor, bytes, offset, bytes.length - offset, offset);
|
|
52
|
+
if (read === 0)
|
|
53
|
+
break;
|
|
54
|
+
offset += read;
|
|
55
|
+
}
|
|
56
|
+
return bytes.subarray(0, offset);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
if (descriptor !== undefined)
|
|
63
|
+
closeSync(descriptor);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Text facade for parsers and legacy callers. Security-sensitive identities
|
|
68
|
+
* should hash the raw bytes returned by createRepoFileBufferReader first, since
|
|
69
|
+
* distinct invalid UTF-8 sequences can decode to the same replacement text. */
|
|
70
|
+
export function createRepoFileReader(root, options = {}) {
|
|
71
|
+
const read = createRepoFileBufferReader(root, options);
|
|
72
|
+
return (file) => read(file)?.toString("utf8") ?? null;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=safeRepoFile.js.map
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* <!--, ;) so a matching STRING literal in code isn't mistaken for intent. (Line-based,
|
|
8
8
|
* so a tagged line that is itself a string literal can still false-positive — advisory.)
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { readdirSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { trackedFiles } from "./git.js";
|
|
13
13
|
import { toPosixTarget } from "../core/paths.js";
|
|
14
|
+
import { createRepoFileReader } from "../core/safeRepoFile.js";
|
|
14
15
|
const EXTS = [
|
|
15
16
|
".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
|
|
16
17
|
".py", ".go", ".rb", ".java", ".rs", ".php", ".cs", ".kt", ".swift", ".scala",
|
|
@@ -42,7 +43,7 @@ function sourceFiles(root) {
|
|
|
42
43
|
if (!SKIP.has(e.name))
|
|
43
44
|
walk(join(dir, e.name), r, depth + 1);
|
|
44
45
|
}
|
|
45
|
-
else if (EXTS.some((x) => e.name.endsWith(x))) {
|
|
46
|
+
else if (e.isFile() && EXTS.some((x) => e.name.endsWith(x))) {
|
|
46
47
|
out.push(r);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -52,14 +53,11 @@ function sourceFiles(root) {
|
|
|
52
53
|
}
|
|
53
54
|
export function extractInlineIntent(root) {
|
|
54
55
|
const out = [];
|
|
56
|
+
const readSourceFile = createRepoFileReader(root);
|
|
55
57
|
for (const rel of sourceFiles(root)) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
content = readFileSync(join(root, rel), "utf8");
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
58
|
+
const content = readSourceFile(rel);
|
|
59
|
+
if (content === null)
|
|
61
60
|
continue;
|
|
62
|
-
}
|
|
63
61
|
if (!content.includes("hunch-"))
|
|
64
62
|
continue; // cheap skip before the per-line scan
|
|
65
63
|
const file = toPosixTarget(rel);
|