@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.
Files changed (59) hide show
  1. package/README.md +96 -1
  2. package/dist/cli/index.js +1238 -396
  3. package/dist/constitution/adapters.js +31 -14
  4. package/dist/constitution/behaviorEvaluator.js +20 -7
  5. package/dist/constitution/behaviorProof.js +3 -2
  6. package/dist/constitution/canonical.js +7 -1
  7. package/dist/constitution/card.js +7 -2
  8. package/dist/constitution/compiler.js +71 -1
  9. package/dist/constitution/correctionPolicyMaterializer.js +496 -0
  10. package/dist/constitution/delta.js +3 -2
  11. package/dist/constitution/evaluator.js +29 -3
  12. package/dist/constitution/experiment.js +96 -5
  13. package/dist/constitution/experimentRunner.js +43 -14
  14. package/dist/constitution/g2BehaviorCandidates.js +49 -26
  15. package/dist/constitution/g2BehaviorDependencies.js +203 -14
  16. package/dist/constitution/g2Candidates.js +1 -1
  17. package/dist/constitution/lifecycle.js +17 -0
  18. package/dist/constitution/plan.js +26 -9
  19. package/dist/constitution/replacementFreeGit.js +67 -0
  20. package/dist/constitution/replay.js +6 -0
  21. package/dist/constitution/replayCache.js +1 -1
  22. package/dist/constitution/replayWorker.js +1 -1
  23. package/dist/constitution/repository.js +141 -5
  24. package/dist/constitution/safeCheckout.js +75 -0
  25. package/dist/constitution/schema.js +30 -5
  26. package/dist/constitution/service.js +74 -14
  27. package/dist/constitution/sourceMutation.js +65 -12
  28. package/dist/constitution/staticGraphBaseline.js +44 -0
  29. package/dist/constitution/structural.js +60 -4
  30. package/dist/core/autoreview.js +1 -1
  31. package/dist/core/canonicalOrder.js +6 -0
  32. package/dist/core/conformance.js +68 -27
  33. package/dist/core/docscan.js +2 -1
  34. package/dist/core/escalations.js +11 -0
  35. package/dist/core/io.js +44 -9
  36. package/dist/core/overlaySafety.js +178 -0
  37. package/dist/core/paths.js +13 -2
  38. package/dist/core/safeRepoFile.js +74 -0
  39. package/dist/extractors/comments.js +6 -8
  40. package/dist/extractors/git.js +1631 -82
  41. package/dist/extractors/indexer.js +86 -47
  42. package/dist/extractors/repoSource.js +390 -0
  43. package/dist/integrations/ciAction.js +10 -2
  44. package/dist/integrations/gitignore.js +44 -5
  45. package/dist/integrations/mergeDriver.js +23 -5
  46. package/dist/integrations/sync.js +61 -5
  47. package/dist/integrations/team.js +666 -23
  48. package/dist/mcp/server.js +261 -34
  49. package/dist/store/db.js +57 -7
  50. package/dist/store/hunchStore.js +92 -11
  51. package/dist/store/jsonStore.js +350 -63
  52. package/dist/store/schema.js +27 -11
  53. package/dist/synthesis/provider.js +13 -4
  54. package/dist/synthesis/synthesize.js +56 -19
  55. package/dist/wiki/graph.js +5 -4
  56. package/dist/wiki/wiki.js +16 -10
  57. package/package.json +15 -3
  58. package/tooling/competitive-watch.mjs +108 -0
  59. 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 { readFileSync, readdirSync } from "node:fs";
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
- let content;
57
- try {
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);