@davesheffer/hunch 1.31.1 → 1.32.1

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 (223) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/automaticReviewMemory.d.ts +13 -0
  3. package/dist/cli/dna.d.ts +2 -0
  4. package/dist/cli/index.d.ts +16 -0
  5. package/dist/cli/index.js +606 -43
  6. package/dist/cli/integrations.d.ts +2 -0
  7. package/dist/cli/integrations.js +5 -2
  8. package/dist/cli/invocation.d.ts +35 -0
  9. package/dist/cli/preflight.d.ts +1 -0
  10. package/dist/cli/reviewMemory.d.ts +4 -0
  11. package/dist/cli/reviewMemoryProvider.d.ts +14 -0
  12. package/dist/cli/serve.d.ts +2 -0
  13. package/dist/cli/taskReport.d.ts +6 -0
  14. package/dist/cli/taskReport.js +128 -0
  15. package/dist/cli/update.d.ts +14 -0
  16. package/dist/client/state.d.ts +248 -0
  17. package/dist/constitution/adapters.d.ts +29 -0
  18. package/dist/constitution/behaviorAttestationBinding.d.ts +3 -0
  19. package/dist/constitution/behaviorEvaluator.d.ts +8 -0
  20. package/dist/constitution/behaviorProof.d.ts +9 -0
  21. package/dist/constitution/behaviorWorkspace.d.ts +25 -0
  22. package/dist/constitution/bootstrap.d.ts +30 -0
  23. package/dist/constitution/canonical.d.ts +15 -0
  24. package/dist/constitution/card.d.ts +58 -0
  25. package/dist/constitution/compiler.d.ts +52 -0
  26. package/dist/constitution/composition.d.ts +10 -0
  27. package/dist/constitution/corpus.d.ts +7 -0
  28. package/dist/constitution/correctionPolicyMaterializer.d.ts +57 -0
  29. package/dist/constitution/delta.d.ts +4 -0
  30. package/dist/constitution/disposition.d.ts +20 -0
  31. package/dist/constitution/evaluator.d.ts +47 -0
  32. package/dist/constitution/experiment.d.ts +466 -0
  33. package/dist/constitution/experimentRunner.d.ts +7 -0
  34. package/dist/constitution/g2.d.ts +209 -0
  35. package/dist/constitution/g2BehaviorAttestation.d.ts +45 -0
  36. package/dist/constitution/g2BehaviorCandidates.d.ts +123 -0
  37. package/dist/constitution/g2BehaviorDependencies.d.ts +83 -0
  38. package/dist/constitution/g2BehaviorMaterialization.d.ts +77 -0
  39. package/dist/constitution/g2BehaviorPolicyMaterializer.d.ts +48 -0
  40. package/dist/constitution/g2CandidateAttestation.d.ts +43 -0
  41. package/dist/constitution/g2Candidates.d.ts +80 -0
  42. package/dist/constitution/g2Drills.d.ts +33 -0
  43. package/dist/constitution/g3.d.ts +311 -0
  44. package/dist/constitution/g3Conformance.d.ts +33 -0
  45. package/dist/constitution/lifecycle.d.ts +25 -0
  46. package/dist/constitution/mutation.d.ts +15 -0
  47. package/dist/constitution/nodeTestEvidence.d.ts +13 -0
  48. package/dist/constitution/plan.d.ts +18 -0
  49. package/dist/constitution/policyRuntime.d.ts +5 -0
  50. package/dist/constitution/proof.d.ts +11 -0
  51. package/dist/constitution/repairPolicies.d.ts +34 -0
  52. package/dist/constitution/replacementFreeGit.d.ts +14 -0
  53. package/dist/constitution/replay.d.ts +40 -0
  54. package/dist/constitution/replayCache.d.ts +14 -0
  55. package/dist/constitution/replayWorker.d.ts +1 -0
  56. package/dist/constitution/repository.d.ts +129 -0
  57. package/dist/constitution/safeCheckout.d.ts +9 -0
  58. package/dist/constitution/schema.d.ts +1763 -0
  59. package/dist/constitution/scorecard.d.ts +219 -0
  60. package/dist/constitution/service.d.ts +398 -0
  61. package/dist/constitution/shadow.d.ts +54 -0
  62. package/dist/constitution/sourceMutation.d.ts +15 -0
  63. package/dist/constitution/staticGraphBaseline.d.ts +13 -0
  64. package/dist/constitution/structural.d.ts +64 -0
  65. package/dist/core/agenthook.d.ts +63 -0
  66. package/dist/core/agenthook.js +1 -0
  67. package/dist/core/automaticReviewMemory.d.ts +60 -0
  68. package/dist/core/autoreview.d.ts +66 -0
  69. package/dist/core/canonicalOrder.d.ts +3 -0
  70. package/dist/core/capturetoken.d.ts +19 -0
  71. package/dist/core/changeIdentity.d.ts +22 -0
  72. package/dist/core/changeProof.d.ts +14 -0
  73. package/dist/core/checkreport.d.ts +140 -0
  74. package/dist/core/commitrepair.d.ts +171 -0
  75. package/dist/core/commitrepair.js +238 -0
  76. package/dist/core/compare.d.ts +21 -0
  77. package/dist/core/config.d.ts +20 -0
  78. package/dist/core/conformance.d.ts +35 -0
  79. package/dist/core/constraintmatch.d.ts +54 -0
  80. package/dist/core/correction.d.ts +42 -0
  81. package/dist/core/correctionStage.d.ts +224 -0
  82. package/dist/core/declarationClusters.d.ts +149 -0
  83. package/dist/core/delivery.d.ts +110 -0
  84. package/dist/core/docanchors.d.ts +32 -0
  85. package/dist/core/docscan.d.ts +26 -0
  86. package/dist/core/drift.d.ts +13 -0
  87. package/dist/core/drift.js +31 -1
  88. package/dist/core/dupdetect.d.ts +43 -0
  89. package/dist/core/escalations.d.ts +129 -0
  90. package/dist/core/escalations.js +165 -0
  91. package/dist/core/events.d.ts +29 -0
  92. package/dist/core/evidenceMap.d.ts +164 -0
  93. package/dist/core/externalImports.d.ts +8 -0
  94. package/dist/core/format.d.ts +13 -0
  95. package/dist/core/glob.d.ts +9 -0
  96. package/dist/core/groundingLag.d.ts +84 -0
  97. package/dist/core/groundingLag.js +16 -2
  98. package/dist/core/groundingMerge.d.ts +15 -0
  99. package/dist/core/groundingMerge.js +95 -0
  100. package/dist/core/hookObservations.d.ts +9 -0
  101. package/dist/core/hookObservations.js +33 -0
  102. package/dist/core/hookcache.d.ts +10 -0
  103. package/dist/core/hookpolicy.d.ts +47 -0
  104. package/dist/core/ids.d.ts +36 -0
  105. package/dist/core/importReview.d.ts +30 -0
  106. package/dist/core/io.d.ts +23 -0
  107. package/dist/core/jsonc.d.ts +4 -0
  108. package/dist/core/landscapeAdoption.d.ts +66 -0
  109. package/dist/core/landscapeDelivery.d.ts +76 -0
  110. package/dist/core/memorylog.d.ts +37 -0
  111. package/dist/core/migrate.d.ts +28 -0
  112. package/dist/core/outcomeExperience.d.ts +89 -0
  113. package/dist/core/overlaySafety.d.ts +24 -0
  114. package/dist/core/overlaySafety.js +7 -1
  115. package/dist/core/paths.d.ts +31 -0
  116. package/dist/core/pipeline.d.ts +289 -0
  117. package/dist/core/premises.d.ts +43 -0
  118. package/dist/core/provenance.d.ts +21 -0
  119. package/dist/core/publication.d.ts +54 -0
  120. package/dist/core/refrepair.d.ts +24 -0
  121. package/dist/core/refrepair.js +1 -1
  122. package/dist/core/relativeImports.d.ts +11 -0
  123. package/dist/core/repair.d.ts +44 -0
  124. package/dist/core/repairqueue.d.ts +40 -0
  125. package/dist/core/repairqueue.js +133 -0
  126. package/dist/core/reviewMemory.d.ts +40 -0
  127. package/dist/core/reviewqueue.d.ts +54 -0
  128. package/dist/core/safeRepoFile.d.ts +20 -0
  129. package/dist/core/served.d.ts +57 -0
  130. package/dist/core/served.js +14 -0
  131. package/dist/core/stateContract.d.ts +979 -0
  132. package/dist/core/stateDelivery.d.ts +78 -0
  133. package/dist/core/stateRecords.d.ts +383 -0
  134. package/dist/core/stats.d.ts +88 -0
  135. package/dist/core/strictgate.d.ts +33 -0
  136. package/dist/core/taskReport.d.ts +226 -0
  137. package/dist/core/taskReport.js +514 -0
  138. package/dist/core/taskReportCapture.d.ts +8 -0
  139. package/dist/core/taskReportCapture.js +36 -0
  140. package/dist/core/taskReportEvidence.d.ts +28 -0
  141. package/dist/core/taskReportEvidence.js +272 -0
  142. package/dist/core/taskReportHook.d.ts +20 -0
  143. package/dist/core/taskReportHook.js +76 -0
  144. package/dist/core/taskReportPaths.d.ts +3 -0
  145. package/dist/core/taskReportPaths.js +23 -0
  146. package/dist/core/taskReportPublic.d.ts +31 -0
  147. package/dist/core/taskReportPublic.js +45 -0
  148. package/dist/core/taskReportRender.d.ts +6 -0
  149. package/dist/core/taskReportRender.js +97 -0
  150. package/dist/core/topics.d.ts +73 -0
  151. package/dist/core/types.d.ts +1239 -0
  152. package/dist/core/version.d.ts +10 -0
  153. package/dist/eval/guards.d.ts +45 -0
  154. package/dist/eval/harness.d.ts +58 -0
  155. package/dist/extractors/adrImport.d.ts +59 -0
  156. package/dist/extractors/comments.d.ts +7 -0
  157. package/dist/extractors/correctionSources.d.ts +11 -0
  158. package/dist/extractors/diff.d.ts +40 -0
  159. package/dist/extractors/git.d.ts +304 -0
  160. package/dist/extractors/git.js +208 -20
  161. package/dist/extractors/helm.d.ts +31 -0
  162. package/dist/extractors/indexer.d.ts +55 -0
  163. package/dist/extractors/landscapeDiscovery.d.ts +41 -0
  164. package/dist/extractors/languages.d.ts +77 -0
  165. package/dist/extractors/nativeTreeSitter.d.ts +16 -0
  166. package/dist/extractors/parse.d.ts +54 -0
  167. package/dist/extractors/php.d.ts +26 -0
  168. package/dist/extractors/repoSource.d.ts +53 -0
  169. package/dist/extractors/testreport.d.ts +36 -0
  170. package/dist/integrations/ciAction.d.ts +8 -0
  171. package/dist/integrations/claudeConfig.d.ts +44 -0
  172. package/dist/integrations/claudemd.d.ts +13 -0
  173. package/dist/integrations/claudemd.js +11 -2
  174. package/dist/integrations/gitignore.d.ts +16 -0
  175. package/dist/integrations/gitignore.js +7 -0
  176. package/dist/integrations/health.d.ts +74 -0
  177. package/dist/integrations/health.js +32 -2
  178. package/dist/integrations/hooks.d.ts +41 -0
  179. package/dist/integrations/hooks.js +92 -66
  180. package/dist/integrations/madrExport.d.ts +39 -0
  181. package/dist/integrations/madrManifest.d.ts +69 -0
  182. package/dist/integrations/mergeDriver.d.ts +3 -0
  183. package/dist/integrations/mergeDriver.js +25 -8
  184. package/dist/integrations/probe.d.ts +4 -0
  185. package/dist/integrations/providers.d.ts +103 -0
  186. package/dist/integrations/scaffold.d.ts +29 -0
  187. package/dist/integrations/sync.d.ts +30 -0
  188. package/dist/integrations/sync.js +3 -2
  189. package/dist/integrations/team.d.ts +70 -0
  190. package/dist/integrations/worktree.d.ts +8 -0
  191. package/dist/mcp/roots.d.ts +16 -0
  192. package/dist/mcp/server.d.ts +36 -0
  193. package/dist/mcp/server.js +74 -22
  194. package/dist/mcp/taskReportTools.d.ts +188 -0
  195. package/dist/mcp/taskReportTools.js +147 -0
  196. package/dist/serve/app.d.ts +36 -0
  197. package/dist/serve/config.d.ts +100 -0
  198. package/dist/serve/writelock.d.ts +22 -0
  199. package/dist/store/changeLedger.d.ts +132 -0
  200. package/dist/store/compact.d.ts +33 -0
  201. package/dist/store/db.d.ts +9 -0
  202. package/dist/store/embedder.d.ts +32 -0
  203. package/dist/store/hunchStore.d.ts +624 -0
  204. package/dist/store/hunchStore.js +6 -3
  205. package/dist/store/jsonStore.d.ts +125 -0
  206. package/dist/store/merge.d.ts +39 -0
  207. package/dist/store/privateMigrate.d.ts +24 -0
  208. package/dist/store/replay.d.ts +56 -0
  209. package/dist/store/schema.d.ts +17 -0
  210. package/dist/store/stateBinding.d.ts +145 -0
  211. package/dist/store/stateCapture.d.ts +15 -0
  212. package/dist/synthesis/cliAdapter.d.ts +22 -0
  213. package/dist/synthesis/initiator.d.ts +13 -0
  214. package/dist/synthesis/provider.d.ts +307 -0
  215. package/dist/synthesis/synthesize.d.ts +117 -0
  216. package/dist/synthesis/tripwires.d.ts +9 -0
  217. package/dist/taskReports.d.ts +79 -0
  218. package/dist/taskReports.js +69 -0
  219. package/dist/wiki/adopt.d.ts +22 -0
  220. package/dist/wiki/graph.d.ts +87 -0
  221. package/dist/wiki/wiki.d.ts +242 -0
  222. package/package.json +6 -5
  223. package/server.json +2 -2
@@ -2,9 +2,9 @@
2
2
  * No LLM here — just parsing what git already knows. */
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { createHash } from "node:crypto";
5
- import { devNull } from "node:os";
5
+ import { devNull, tmpdir } from "node:os";
6
6
  import { isAbsolute, resolve, join, basename, dirname, relative, sep } from "node:path";
7
- import { mkdirSync, rmSync, statSync, lstatSync, realpathSync, readFileSync, renameSync, readdirSync } from "node:fs";
7
+ import { mkdtempSync, openSync, closeSync, readSync, mkdirSync, rmSync, statSync, lstatSync, realpathSync, readFileSync, renameSync, readdirSync } from "node:fs";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { MEMLOG_FORMAT } from "../core/memorylog.js";
10
10
  import { hunchAttributesAreSafe, hunchTreeAttributesAreSafe, safeOverlayTree } from "../core/overlaySafety.js";
@@ -496,6 +496,12 @@ export function stableRepositoryName(root) {
496
496
  return `git:${roots.join("+")}`;
497
497
  return basename(mainWorktreeRoot(root));
498
498
  }
499
+ function observeGit(observer, event) {
500
+ try {
501
+ observer?.(event);
502
+ }
503
+ catch { /* observation cannot change a Git outcome */ }
504
+ }
499
505
  const CAPTURE_REMOTE_TIMEOUT_MS = 15_000;
500
506
  const READ_REMOTE_TIMEOUT_MS = 5_000;
501
507
  // A capture can spend roughly 90s in commit + bounded merge/push/retry seams.
@@ -698,6 +704,11 @@ export function commitAndPushHunch(hunchDir, message, opts) {
698
704
  }
699
705
  if (!committed)
700
706
  return null;
707
+ if (opts.observe) {
708
+ const commitSha = headShaWithEnv(hunchDir, env);
709
+ if (/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commitSha))
710
+ observeGit(opts.observe, { kind: "committed", commitSha });
711
+ }
701
712
  if (opts.push !== false) {
702
713
  // The overlay remote is mutable process state. Re-prove the publication
703
714
  // boundary after the local commit and BEFORE pull: hooks or another
@@ -715,7 +726,7 @@ export function commitAndPushHunch(hunchDir, message, opts) {
715
726
  // proof immediately before its push seam. Repeating the same expensive proof
716
727
  // twice here adds no intervening mutation boundary and materially slows large
717
728
  // graph refreshes on process-spawn-sensitive platforms such as Windows.
718
- if (pushWithOneRemoteAdvanceRetry(hunchDir, env, opts.protectedRepoRoot, CAPTURE_REMOTE_TIMEOUT_MS, opts.remote))
729
+ if (pushWithOneRemoteAdvanceRetry(hunchDir, env, opts.protectedRepoRoot, CAPTURE_REMOTE_TIMEOUT_MS, opts.remote, opts.observe))
719
730
  return "pushed";
720
731
  }
721
732
  return "committed";
@@ -828,7 +839,13 @@ function isDerivedStoreArtifact(relativeName) {
828
839
  // `hunch serve` flushes INSIDE its cross-process write lock, so the lock file is always
829
840
  // staged alongside the record; treating it as a violation made every served write skip
830
841
  // the commit quietly and report durability "local" forever (1.26.0/1.26.1).
831
- || relativeName === "write.lock";
842
+ || relativeName === "write.lock"
843
+ // The post-merge hook's detected-but-unconfirmed repair queue and its
844
+ // rejected-match tombstones (repairqueue.ts): clone-local scratch, never a
845
+ // memory record — must never ride a public flush's `git add .` or an
846
+ // overlay's force-add allowlist into shared/pushed memory.
847
+ || relativeName === "pending-commit-repairs.json"
848
+ || relativeName === "dropped-commit-repairs.json";
832
849
  }
833
850
  /** Enumerate ordinary JSON files already contained under an overlay. Push-capable
834
851
  * stores force-add this exact allowlist so remote .gitignore, info/exclude, or an
@@ -1328,7 +1345,91 @@ function upstreamSha(hunchDir, env) {
1328
1345
  return "";
1329
1346
  }
1330
1347
  }
1331
- function tryPush(hunchDir, env, timeoutMs, contract) {
1348
+ /** Keep observation output limits out of the primary Git process. A bounded
1349
+ * exec pipe can terminate an otherwise successful multi-ref push on overflow.
1350
+ * Spool privately for the duration of the command, read at most 1 MB afterward,
1351
+ * and discard oversized/unavailable output without changing its exit result. */
1352
+ function runObservedPush(args, env, timeoutMs, observe) {
1353
+ if (!observe) {
1354
+ execFileSync("git", args, { stdio: "ignore", env, timeout: timeoutMs });
1355
+ return null;
1356
+ }
1357
+ let directory, descriptor;
1358
+ try {
1359
+ directory = mkdtempSync(join(tmpdir(), "hunch-push-observation-"));
1360
+ descriptor = openSync(join(directory, "stdout"), "wx+", 0o600);
1361
+ }
1362
+ catch {
1363
+ if (directory) {
1364
+ try {
1365
+ rmSync(directory, { recursive: true, force: true });
1366
+ }
1367
+ catch { }
1368
+ }
1369
+ execFileSync("git", args, { stdio: "ignore", env, timeout: timeoutMs });
1370
+ return null;
1371
+ }
1372
+ try {
1373
+ execFileSync("git", args, { stdio: ["ignore", descriptor, "ignore"], env, timeout: timeoutMs });
1374
+ try {
1375
+ const buffer = Buffer.alloc(1_000_001);
1376
+ const bytes = readSync(descriptor, buffer, 0, buffer.length, 0);
1377
+ return bytes <= 1_000_000 ? buffer.subarray(0, bytes).toString("utf8") : null;
1378
+ }
1379
+ catch {
1380
+ return null;
1381
+ }
1382
+ }
1383
+ finally {
1384
+ if (descriptor !== undefined) {
1385
+ try {
1386
+ closeSync(descriptor);
1387
+ }
1388
+ catch { }
1389
+ }
1390
+ try {
1391
+ rmSync(directory, { recursive: true, force: true });
1392
+ }
1393
+ catch { /* no effect on the Git outcome */ }
1394
+ }
1395
+ }
1396
+ /** Porcelain identifies the destination Git actually used, preserving legacy
1397
+ * push refspecs. New/up-to-date refs lack an OID, so confirm that exact remote
1398
+ * ref with a bounded read. Never infer publication from a post-push local HEAD. */
1399
+ function observePushResult(hunchDir, env, output, observer) {
1400
+ let destination = "", count = 0;
1401
+ const deadline = Date.now() + 2_000;
1402
+ for (const line of output.split("\n")) {
1403
+ if (line.startsWith("To ")) {
1404
+ destination = line.slice(3);
1405
+ continue;
1406
+ }
1407
+ const status = /^([ *+=])\t[^\t]+:(refs\/[^\t]+)\t(.+)$/.exec(line);
1408
+ if (!status || ++count > 32)
1409
+ continue;
1410
+ const ref = status[2];
1411
+ if (/[\x00-\x20\x7f]/.test(ref))
1412
+ continue;
1413
+ const range = /^(?:[a-f0-9]{40}|[a-f0-9]{64})\.{2,3}([a-f0-9]{40}|[a-f0-9]{64})(?: |$)/.exec(status[3]);
1414
+ if (range) {
1415
+ observeGit(observer, { kind: "published", commitSha: range[1], ref, basis: "push-status" });
1416
+ continue;
1417
+ }
1418
+ if (!destination || /[\x00-\x1f\x7f]/.test(destination) || Date.now() >= deadline)
1419
+ continue;
1420
+ try {
1421
+ const result = execFileSync("git", ["-C", hunchDir, "ls-remote", "--refs", "--exit-code", "--", destination, ref], {
1422
+ env, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: Math.max(1, deadline - Date.now()), maxBuffer: 16_384,
1423
+ }).trim();
1424
+ const rows = result.split("\n").filter(row => row.split("\t")[1] === ref);
1425
+ const commitSha = rows.length === 1 ? rows[0].split("\t")[0] : "";
1426
+ if (/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commitSha))
1427
+ observeGit(observer, { kind: "published", commitSha, ref, basis: "remote-ref-confirmed" });
1428
+ }
1429
+ catch { /* no exact publication proof; the original push outcome stands */ }
1430
+ }
1431
+ }
1432
+ function tryPush(hunchDir, env, timeoutMs, contract, observe) {
1332
1433
  if (!contractReady(contract))
1333
1434
  return false;
1334
1435
  const hooksDir = disabledHooksDir(hunchDir);
@@ -1337,11 +1438,17 @@ function tryPush(hunchDir, env, timeoutMs, contract) {
1337
1438
  try {
1338
1439
  if (contract && !setContractUpstream(hunchDir, contract, env))
1339
1440
  return false;
1441
+ const observeArgs = observe ? ["-c", "core.abbrev=no"] : [];
1442
+ const porcelain = observe ? ["--porcelain"] : [];
1340
1443
  const args = contract
1341
- ? ["-C", hunchDir, "-c", `core.hooksPath=${hooksDir}`, "push", "--receive-pack=git-receive-pack", contract.pushUrl, `HEAD:${contract.ref}`]
1342
- : ["-C", hunchDir, "-c", `core.hooksPath=${hooksDir}`, "push"];
1343
- execFileSync("git", args, { stdio: "ignore", env: contract ? boundedTeamEnv(env) : env, timeout: timeoutMs });
1344
- return contractReady(contract);
1444
+ ? ["-C", hunchDir, "-c", `core.hooksPath=${hooksDir}`, ...observeArgs, "push", ...porcelain, "--receive-pack=git-receive-pack", contract.pushUrl, `HEAD:${contract.ref}`]
1445
+ : ["-C", hunchDir, "-c", `core.hooksPath=${hooksDir}`, ...observeArgs, "push", ...porcelain];
1446
+ const output = runObservedPush(args, contract ? boundedTeamEnv(env) : env, timeoutMs, !!observe);
1447
+ if (!contractReady(contract))
1448
+ return false;
1449
+ if (observe && output !== null)
1450
+ observePushResult(hunchDir, contract ? boundedTeamEnv(env) : env, output, observe);
1451
+ return true;
1345
1452
  }
1346
1453
  catch {
1347
1454
  return false;
@@ -1353,7 +1460,7 @@ function tryPush(hunchDir, env, timeoutMs, contract) {
1353
1460
  * remote), proves that remote has no refs, snapshots its identity across the
1354
1461
  * network check, and uses a non-force push so a concurrent first writer wins
1355
1462
  * safely instead of being overwritten. */
1356
- function establishEmptyRemoteUpstream(hunchDir, env, protectedRepoRoot, timeoutMs, contract) {
1463
+ function establishEmptyRemoteUpstream(hunchDir, env, protectedRepoRoot, timeoutMs, contract, observe) {
1357
1464
  if (unsafeOverlayPublication(hunchDir, protectedRepoRoot)
1358
1465
  || !hunchWorktreeClean(hunchDir, env)
1359
1466
  || !contractReady(contract))
@@ -1365,7 +1472,7 @@ function establishEmptyRemoteUpstream(hunchDir, env, protectedRepoRoot, timeoutM
1365
1472
  // Exact URL + exact canonical ref + non-force push. If another teammate wins
1366
1473
  // the first-writer race after the empty proof, Git rejects this safely and the
1367
1474
  // bounded retry path fetches/merges that winner.
1368
- return tryPush(hunchDir, env, timeoutMs, contract);
1475
+ return tryPush(hunchDir, env, timeoutMs, contract, observe);
1369
1476
  }
1370
1477
  let branch = "";
1371
1478
  let remotes = [];
@@ -1424,11 +1531,9 @@ function establishEmptyRemoteUpstream(hunchDir, env, protectedRepoRoot, timeoutM
1424
1531
  const hooksDir = disabledHooksDir(hunchDir);
1425
1532
  if (!hooksDir)
1426
1533
  return false;
1427
- execFileSync("git", ["-C", hunchDir, "-c", `core.hooksPath=${hooksDir}`, "push", "--set-upstream", remote, `HEAD:refs/heads/${branch}`], {
1428
- stdio: "ignore",
1429
- env,
1430
- timeout: timeoutMs,
1431
- });
1534
+ const output = runObservedPush(["-C", hunchDir, "-c", `core.hooksPath=${hooksDir}`, ...(observe ? ["-c", "core.abbrev=no"] : []), "push", ...(observe ? ["--porcelain"] : []), "--set-upstream", remote, `HEAD:refs/heads/${branch}`], env, timeoutMs, !!observe);
1535
+ if (observe && output !== null)
1536
+ observePushResult(hunchDir, env, output, observe);
1432
1537
  return true;
1433
1538
  }
1434
1539
  catch {
@@ -1438,15 +1543,15 @@ function establishEmptyRemoteUpstream(hunchDir, env, protectedRepoRoot, timeoutM
1438
1543
  /** Push once, then retry exactly once only when a bounded pull proves the upstream
1439
1544
  * advanced during the first push seam. Offline/auth/hook failures with an unchanged
1440
1545
  * upstream never loop, and every remote mutation re-proves the publication boundary. */
1441
- function pushWithOneRemoteAdvanceRetry(hunchDir, env, protectedRepoRoot, timeoutMs, contract) {
1546
+ function pushWithOneRemoteAdvanceRetry(hunchDir, env, protectedRepoRoot, timeoutMs, contract, observe) {
1442
1547
  if (unsafeOverlayPublication(hunchDir, protectedRepoRoot) || !contractReady(contract))
1443
1548
  return false;
1444
1549
  const before = contract
1445
1550
  ? gitSafeWithEnv(["rev-parse", "--verify", TEAM_FETCH_REF], hunchDir, env)
1446
1551
  : upstreamSha(hunchDir, env);
1447
1552
  if (!before)
1448
- return establishEmptyRemoteUpstream(hunchDir, env, protectedRepoRoot, timeoutMs, contract);
1449
- if (tryPush(hunchDir, env, timeoutMs, contract))
1553
+ return establishEmptyRemoteUpstream(hunchDir, env, protectedRepoRoot, timeoutMs, contract, observe);
1554
+ if (tryPush(hunchDir, env, timeoutMs, contract, observe))
1450
1555
  return true;
1451
1556
  if (unsafeOverlayPublication(hunchDir, protectedRepoRoot) || !contractReady(contract))
1452
1557
  return false;
@@ -1458,7 +1563,7 @@ function pushWithOneRemoteAdvanceRetry(hunchDir, env, protectedRepoRoot, timeout
1458
1563
  return false;
1459
1564
  if (unsafeOverlayPublication(hunchDir, protectedRepoRoot) || !contractReady(contract))
1460
1565
  return false;
1461
- return tryPush(hunchDir, env, timeoutMs, contract);
1566
+ return tryPush(hunchDir, env, timeoutMs, contract, observe);
1462
1567
  }
1463
1568
  export function pullHunchStatus(hunchDir, opts = {}) {
1464
1569
  const env = foreignRepoEnv({
@@ -2068,6 +2173,89 @@ export function commitChanges(sha, cwd) {
2068
2173
  }
2069
2174
  return out;
2070
2175
  }
2176
+ /** Default cap on how many commits mergeRangeChanges will inspect. A `git
2177
+ * pull` bringing in an unusually large number of commits (hundreds) would
2178
+ * otherwise spawn one `diff-tree` per commit with no bound — cheap insurance
2179
+ * against an unbounded background hook run, not a hard product limit. */
2180
+ const DEFAULT_MERGE_RANGE_CAP = 500;
2181
+ /** Every commit newly reachable in `oldRef..newRef` (capped to the most
2182
+ * recent `maxCount`), with the files each one changed (rename-aware, via
2183
+ * commitChanges). Used to find the commit a squash-merge produced from a
2184
+ * set of now-orphaned source-branch commits. */
2185
+ export function mergeRangeChanges(oldRef, newRef, cwd, maxCount = DEFAULT_MERGE_RANGE_CAP) {
2186
+ const shas = gitSafe(["rev-list", "--abbrev-commit", `--max-count=${maxCount}`, `${oldRef}..${newRef}`], cwd).split("\n").filter(Boolean).reverse();
2187
+ return shas.map((sha) => ({
2188
+ sha,
2189
+ // A deletion is never a sensible repair target — a decision's provenance
2190
+ // should never resolve to the commit that removed the file it's about.
2191
+ files: commitChanges(sha, cwd).filter((c) => c.status !== "deleted").map((c) => c.after ?? c.before).filter((f) => !!f),
2192
+ }));
2193
+ }
2194
+ /** Batched existence check for many commit shas at once — one `git cat-file
2195
+ * --batch-check` process instead of one `rev-parse --verify` per sha. Used
2196
+ * by drift.ts's default commitResolvable so hunch drift/heal/doctor spawn a
2197
+ * single git process regardless of how many decisions carry a commit, not
2198
+ * one per decision. Returns the subset of `shas` that resolve to a real
2199
+ * commit object, or `null` if the check itself failed to run (not a git
2200
+ * repo, git missing, timeout) — distinct from an empty set, which means
2201
+ * "checked, and none of them resolve." A caller that collapsed those two
2202
+ * would flag every commit as unresolvable on a transient environment error
2203
+ * instead of failing open. */
2204
+ export function commitsExist(shas, cwd) {
2205
+ const resolvable = new Set();
2206
+ if (!shas.length)
2207
+ return resolvable;
2208
+ // Same shape guard as commitRepairStatus. Without it, a malformed value
2209
+ // embedding its own newline would count as more input lines than entries
2210
+ // in `shas`, shifting every later sha's output line and misclassifying it.
2211
+ const valid = shas.filter((sha) => /^[0-9a-f]{7,64}$/i.test(sha));
2212
+ if (!valid.length)
2213
+ return resolvable;
2214
+ let raw;
2215
+ try {
2216
+ raw = execFileSync("git", ["-C", cwd, "cat-file", "--batch-check"], {
2217
+ encoding: "utf8",
2218
+ input: valid.map((sha) => `${sha}^{commit}\n`).join(""),
2219
+ stdio: ["pipe", "pipe", "ignore"],
2220
+ timeout: 10_000,
2221
+ });
2222
+ }
2223
+ catch {
2224
+ return null;
2225
+ }
2226
+ const lines = raw.split("\n").filter(Boolean);
2227
+ for (let i = 0; i < valid.length && i < lines.length; i++) {
2228
+ if (!lines[i].endsWith(" missing"))
2229
+ resolvable.add(valid[i]);
2230
+ }
2231
+ return resolvable;
2232
+ }
2233
+ /** Classifies `commit` relative to `ref` for squash-merge repair eligibility.
2234
+ * "orphaned" (exists in this repo, but is not an ancestor of `ref`) is the
2235
+ * ONLY status planCommitRepair should ever act on. "unresolvable" (the
2236
+ * commit doesn't exist here at all) is explicitly out of repair scope —
2237
+ * drift.ts's commit-unresolvable kind is what flags that case, because
2238
+ * there's no reliable signal left to match a replacement against. "unknown"
2239
+ * (the git command itself failed to run — timeout, git missing) must never
2240
+ * be treated as repair-eligible; collapsing "false" and "error" into one
2241
+ * boolean is exactly the bug this type exists to prevent. */
2242
+ export function commitRepairStatus(commit, ref, cwd) {
2243
+ if (!/^[0-9a-f]{7,64}$/i.test(commit))
2244
+ return "unresolvable";
2245
+ try {
2246
+ execFileSync("git", ["-C", cwd, "rev-parse", "--verify", "--quiet", `${commit}^{commit}`], { stdio: "ignore", timeout: 5_000 });
2247
+ }
2248
+ catch (e) {
2249
+ return typeof e.status === "number" ? "unresolvable" : "unknown";
2250
+ }
2251
+ try {
2252
+ execFileSync("git", ["-C", cwd, "merge-base", "--is-ancestor", commit, ref], { stdio: "ignore", timeout: 5_000 });
2253
+ return "current";
2254
+ }
2255
+ catch (e) {
2256
+ return e.status === 1 ? "orphaned" : "unknown";
2257
+ }
2258
+ }
2071
2259
  /** Machine-generated paths that carry no design "why" — lockfiles, build output,
2072
2260
  * vendored deps, snapshots, source maps. Excluded from synthesis diffs via git
2073
2261
  * pathspec BEFORE git assembles/orders the patch: a huge lockfile sorts ahead of
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Deterministic text scan for Helm's `define`/`include`/`template` Go-template
3
+ * actions — NOT a Go-template parser, and NOT a tree-sitter query. tree-sitter-yaml
4
+ * has no notion of `{{ }}` content: parsing `{{ include "x" . }}` inside a real
5
+ * YAML mapping value produces only stray `{` flow-mapping-open tokens, with the
6
+ * enclosed text lost to ERROR recovery (verified directly against this repo's
7
+ * tree-sitter-yaml bundle). There is nothing for a tree-sitter query to capture,
8
+ * so this runs as a sidecar text scan over the raw source — invoked only for
9
+ * files under a Helm chart (indexer.ts's chart-root detection), never for
10
+ * arbitrary YAML.
11
+ *
12
+ * Known bounded limitation: this is a token scan, not a full parser. A literal
13
+ * `}}` inside a quoted argument, or an include/define-shaped string inside a
14
+ * `{{/* comment *}}`, can misattribute a byte range or produce a phantom call.
15
+ * Both are bounded failure modes (a stray reference to a real symbol name, or a
16
+ * slightly-long symbol range) — the same class of accepted limitation
17
+ * `toleratedErrorScopes` documents for the tree-sitter grammars, not a silent gap.
18
+ * The chart-wide `importedFiles` widening this module's output flows through
19
+ * (indexer.ts) also lets a YAML alias resolve to an anchor in a sibling chart
20
+ * file, even though YAML anchors are properly document-scoped; this only fires
21
+ * when the alias has no matching anchor in its own file (i.e. only on input
22
+ * that's already invalid YAML on its own terms), so it's bounded, but it's a
23
+ * real, disclosed side effect of the chart-scoping mechanism, not something to
24
+ * silently rely on.
25
+ */
26
+ import { type ParsedSymbol, type ParsedCall } from "./parse.js";
27
+ export interface HelmExtraction {
28
+ symbols: ParsedSymbol[];
29
+ calls: ParsedCall[];
30
+ }
31
+ export declare function extractHelmDirectives(source: string): HelmExtraction;
@@ -0,0 +1,55 @@
1
+ import type { HunchStore } from "../store/hunchStore.js";
2
+ import { type Symbol, type Edge, type Component } from "../core/types.js";
3
+ import { type RepoScanSource, type RepoScanSourceIdentity, type RepoSourceIssue } from "./repoSource.js";
4
+ export interface IndexResult {
5
+ files: number;
6
+ symbols: number;
7
+ edges: number;
8
+ components: number;
9
+ /** Files that could not be parsed (read error / oversized / extraction error). */
10
+ skipped: number;
11
+ coverage: Array<{
12
+ language: string;
13
+ eligible: number;
14
+ parsed: number;
15
+ skipped: number;
16
+ reasons: Record<string, number>;
17
+ }>;
18
+ }
19
+ export interface RepoScan {
20
+ result: IndexResult;
21
+ symbols: Symbol[];
22
+ edges: Edge[];
23
+ components: Component[];
24
+ source: RepoScanSourceIdentity & {
25
+ content_hash: string;
26
+ };
27
+ issues: RepoSourceIssue[];
28
+ }
29
+ export interface ScanRepoOptions {
30
+ churn?: boolean;
31
+ source?: RepoScanSource;
32
+ }
33
+ export interface IndexRepoOptions {
34
+ churn?: boolean;
35
+ /** Explicit immutable source for setup paths that must never persist checkout
36
+ * bytes. Normal durable refreshes should use requireClean instead. */
37
+ source?: RepoScanSource;
38
+ /** Production publication paths set this to prove graph bytes came only from
39
+ * committed code. Library fixtures and disposable replay checkouts opt in. */
40
+ requireClean?: boolean;
41
+ /** Authoritative replay/proof paths cannot represent a partial graph. Reject
42
+ * every read, path, mode, or parse issue before writing derived JSON. */
43
+ requireComplete?: boolean;
44
+ }
45
+ /** Read-only policy evaluators must never turn an omitted source file into a
46
+ * false satisfied receipt. Call this after scanRepo when the consumer cannot
47
+ * represent partial-graph uncertainty directly. */
48
+ export declare function assertCompleteRepoScan(scan: RepoScan): void;
49
+ /** Derive the current repository graph without writing JSON or rebuilding SQLite.
50
+ * Read-only gates use this so checking changed code can never rewrite or publish
51
+ * the durable graph merely by inspecting it. Existing public graph records remain
52
+ * inputs for the same churn/component enrichment semantics as a persisted index. */
53
+ export declare function scanRepo(store: HunchStore, root: string, opts?: ScanRepoOptions): RepoScan;
54
+ /** Persist one pure scan into the Git-native source of truth. */
55
+ export declare function indexRepo(store: HunchStore, root: string, opts?: IndexRepoOptions): IndexResult;
@@ -0,0 +1,41 @@
1
+ import { type Edge, type Resource } from "../core/types.js";
2
+ export declare const LANDSCAPE_DISCOVERY_SCHEMA_VERSION: "hunch.landscape-discovery/1";
3
+ export declare const LANDSCAPE_CANDIDATE_SCHEMA_VERSION: "hunch.landscape-candidate/1";
4
+ export type LandscapeEvidenceKind = "package_manifest" | "git_remote" | "git_history" | "submodule_declaration" | "mcp_declaration" | "ci_declaration" | "deployment_declaration" | "api_declaration" | "migration_declaration" | "ownership_declaration" | "operations_declaration" | "dashboard_declaration" | "slo_declaration";
5
+ export interface LandscapeCandidateEvidence {
6
+ kind: LandscapeEvidenceKind;
7
+ sourcePath: string;
8
+ sourceField: string;
9
+ sourceRevision: string;
10
+ sourceContentHash: string;
11
+ }
12
+ export interface LandscapeCandidate<T extends Resource | Edge> {
13
+ schema: typeof LANDSCAPE_CANDIDATE_SCHEMA_VERSION;
14
+ authority: "candidate";
15
+ record: T;
16
+ evidence: LandscapeCandidateEvidence[];
17
+ candidateHash: string;
18
+ }
19
+ export type LandscapeDiscoveryIssueCode = "manifest_missing" | "manifest_invalid" | "manifest_oversized" | "manifest_mode" | "manifest_path" | "manifest_limit" | "workspace_pattern_invalid" | "package_name_missing" | "package_name_invalid" | "package_identity_conflict" | "package_dependency_invalid" | "package_dependency_limit" | "repository_identity_conflict" | "submodule_declaration_invalid" | "submodule_declaration_oversized" | "submodule_declaration_mode" | "submodule_declaration_limit" | "mcp_config_invalid" | "mcp_config_oversized" | "mcp_config_mode" | "mcp_server_name_invalid" | "mcp_declaration_invalid" | "mcp_declaration_conflict" | "mcp_declaration_limit" | "delivery_declaration_invalid" | "delivery_declaration_oversized" | "delivery_declaration_mode" | "delivery_declaration_path" | "delivery_declaration_limit" | "delivery_declaration_conflict" | "api_declaration_invalid" | "api_declaration_oversized" | "api_declaration_mode" | "api_declaration_path" | "api_declaration_limit" | "migration_declaration_invalid" | "migration_declaration_oversized" | "migration_declaration_mode" | "migration_declaration_path" | "migration_declaration_limit" | "ownership_declaration_invalid" | "ownership_declaration_oversized" | "ownership_declaration_mode" | "ownership_declaration_limit" | "operations_declaration_invalid" | "operations_declaration_oversized" | "operations_declaration_mode" | "operations_declaration_path" | "operations_declaration_limit" | "dashboard_declaration_invalid" | "dashboard_declaration_oversized" | "dashboard_declaration_mode" | "dashboard_declaration_path" | "dashboard_declaration_limit" | "slo_declaration_invalid" | "slo_declaration_oversized" | "slo_declaration_mode" | "slo_declaration_path" | "slo_declaration_limit";
20
+ export interface LandscapeDiscoveryIssue {
21
+ code: LandscapeDiscoveryIssueCode;
22
+ sourcePath: string;
23
+ sourceField: string;
24
+ detail: string;
25
+ }
26
+ export interface LandscapeDiscoveryResult {
27
+ schema: typeof LANDSCAPE_DISCOVERY_SCHEMA_VERSION;
28
+ authority: "candidate";
29
+ sourceRevision: string;
30
+ repositoryRootIdentity: string;
31
+ resources: Array<LandscapeCandidate<Resource>>;
32
+ relationships: Array<LandscapeCandidate<Edge>>;
33
+ issues: LandscapeDiscoveryIssue[];
34
+ discoveryHash: string;
35
+ }
36
+ /** Canonical content identity shared by discovery and its review/adoption seam. */
37
+ export declare function landscapeContentHash(value: unknown): string;
38
+ /** Discover an exact, reviewable repository candidate fragment. This function
39
+ * never writes Hunch graph state: candidate authority remains explicit until a
40
+ * normal review/capture path accepts the records. */
41
+ export declare function discoverRepositoryLandscape(root: string, ref?: string): LandscapeDiscoveryResult;
@@ -0,0 +1,77 @@
1
+ import type { Edge } from "../core/types.js";
2
+ export type ParsedSymbolKind = "function" | "method" | "class" | "interface" | "trait" | "enum" | "type" | "variable" | "file";
3
+ export interface LanguageSpec {
4
+ /** Stable id, also used as the grammar-bundle cache key by parse.ts. */
5
+ id: string;
6
+ extensions: string[];
7
+ /** Cache key parse.ts's bundleFor() uses (one grammar+query pair may serve
8
+ * several extensions, e.g. tsx serves both .tsx and .jsx). */
9
+ grammarKey: string;
10
+ /** Lazily returns the tree-sitter Language object for this spec. */
11
+ loadGrammar(): unknown;
12
+ /** Tree-sitter query source capturing every construct this language cares about. */
13
+ query: string;
14
+ /** Node types ascendToDef() walks up to when resolving a name capture's enclosing def. */
15
+ defNodeTypes: Set<string>;
16
+ /** Query capture-name (ending in ".def") -> the ParsedSymbolKind it represents. */
17
+ defKindOf: Record<string, ParsedSymbolKind>;
18
+ /** Query capture-name (ending in ".name") -> the matching ".def" capture-name. */
19
+ nameToDef: Record<string, string>;
20
+ /** Optional semantic relationship captures. The generic parser/indexer emit
21
+ * these through the normal graph path; languages only describe syntax. */
22
+ relationKindOf?: Record<string, {
23
+ edgeType: Edge["type"];
24
+ label: string;
25
+ }>;
26
+ /** Common builtin/stdlib method names. Member calls to these (e.g. `arr.map(...)`)
27
+ * must NOT create call edges to unrelated repo symbols that happen to share the
28
+ * name (DESIGN: keep the graph clean). */
29
+ builtinMethods: Set<string>;
30
+ /** Direct-call builtins for languages such as PHP. These never become repo
31
+ * call edges merely because a project symbol shares the runtime name. */
32
+ builtinFunctions?: Set<string>;
33
+ /** Edge type emitted for this language's calls-list entries (e.g. YAML's
34
+ * alias->anchor references aren't function calls). Defaults to "calls"
35
+ * when omitted — every language before YAML. */
36
+ referenceEdgeType?: Edge["type"];
37
+ /** Fallback name for a ".def" capture with no paired ".name" capture (e.g.
38
+ * a whole-file root symbol with no natural identifier of its own). Omit
39
+ * for a language where every ".def" always has a name capture — parse.ts
40
+ * then keeps dropping unnamed defs, the current behavior for TS/Python. */
41
+ fallbackDefName?: (file: string) => string;
42
+ /** Ancestor shapes in which an ERROR node is a known limitation of THIS grammar
43
+ * rather than a real syntax error. parse.ts forgives an error only when some
44
+ * ancestor has type `node` AND that ancestor's own parent has type `parentIs` —
45
+ * the pair is what keeps the tolerance narrow. Omit for a language with no known
46
+ * grammar false positives; that spec then stays strictly fail-closed. */
47
+ toleratedErrorScopes?: ReadonlyArray<{
48
+ readonly node: string;
49
+ readonly parentIs: string;
50
+ }>;
51
+ /** Patterns whose presence anywhere in the source mean "this isn't actually
52
+ * {id} text yet — it's a template that renders to {id} later" (Go/Jinja/Helm
53
+ * delimiters in a .yaml file, e.g.). parse.ts still runs the real parse — a
54
+ * genuine anchor/symbol elsewhere in the file is still extracted, same as any
55
+ * other {id} file — but no longer lets the resulting ERROR nodes fail-close
56
+ * the whole-repo scan completeness gate the way toleratedErrorScopes protects
57
+ * narrower, per-grammar false positives (issue #33). Keep these narrow: a
58
+ * pattern that also matches ordinary, always-valid {id} syntax (e.g. GitHub
59
+ * Actions' `${{ }}` expressions) would silently disable the fail-closed
60
+ * guarantee for files that were never templated at all. */
61
+ templatingMarkers?: readonly RegExp[];
62
+ /** Extensions within this language where EVERY file is inherently a template
63
+ * regardless of content — unlike templatingMarkers, this never content-sniffs.
64
+ * The extension alone means "this file is a template, parseable must never
65
+ * become false for it," matching Helm's `_helpers.tpl` convention where the
66
+ * file may be pure Go-template text with no YAML structure of its own at all. */
67
+ alwaysTemplatedExtensions?: readonly string[];
68
+ }
69
+ export declare const LANGUAGES: LanguageSpec[];
70
+ export declare const CODE_EXTENSIONS: string[];
71
+ export declare function languageFor(file: string): LanguageSpec | null;
72
+ /** Prose formats worth drafting a decision from even though no LanguageSpec parses
73
+ * them — no grammar, no symbols/edges, just eligible input to synthesis (issue #12). */
74
+ export declare const PROSE_EXTENSIONS: string[];
75
+ /** Broader than languageFor: "is this worth reasoning about" (synthesis input)
76
+ * rather than "can tree-sitter parse this" (symbol/dependency extraction). */
77
+ export declare function isSubstantive(file: string): boolean;
@@ -0,0 +1,16 @@
1
+ import type TreeSitterParser from "tree-sitter";
2
+ export interface NativeTreeSitterRuntime {
3
+ Parser: typeof TreeSitterParser;
4
+ typescript: unknown;
5
+ tsx: unknown;
6
+ python: unknown;
7
+ go: unknown;
8
+ php: unknown;
9
+ yaml: unknown;
10
+ }
11
+ /** Load all native tree-sitter addons (the parser runtime + every grammar) from
12
+ * process-owned temp copies. Windows keeps loaded `.node` files locked for the
13
+ * process lifetime; redirecting the upstream loaders means npm can replace the
14
+ * installed package during an active MCP session without killing that session
15
+ * or falling back to a stale binary. */
16
+ export declare function loadNativeTreeSitter(): NativeTreeSitterRuntime;
@@ -0,0 +1,54 @@
1
+ import type { Edge } from "../core/types.js";
2
+ import { type ParsedSymbolKind } from "./languages.js";
3
+ export type { ParsedSymbolKind } from "./languages.js";
4
+ export interface ParsedSymbol {
5
+ name: string;
6
+ kind: ParsedSymbolKind;
7
+ startByte: number;
8
+ endByte: number;
9
+ loc: number;
10
+ bodyText: string;
11
+ }
12
+ export interface ParsedCall {
13
+ callee: string;
14
+ atByte: number;
15
+ endByte: number;
16
+ /** true for `x.foo()` (property access), false for a direct `foo()` call. */
17
+ member: boolean;
18
+ }
19
+ export interface ParsedRelation {
20
+ target: string;
21
+ atByte: number;
22
+ endByte: number;
23
+ edgeType: Edge["type"];
24
+ label: string;
25
+ }
26
+ export interface ParsedFile {
27
+ symbols: ParsedSymbol[];
28
+ imports: string[];
29
+ calls: ParsedCall[];
30
+ relations: ParsedRelation[];
31
+ namespace: string | null;
32
+ parseable: boolean;
33
+ }
34
+ /** Cap on a stored symbol's bodyText — large enough for review context, small
35
+ * enough that a huge function/file doesn't bloat every JSON symbol record. */
36
+ export declare const MAX_BODY_TEXT_CHARS = 4000;
37
+ export declare function parseSource(file: string, source: string): ParsedFile | null;
38
+ /** Map each call site to the innermost symbol whose byte-range contains it.
39
+ * Keyed by the symbol's position (index) in `parsed.symbols` — NOT its
40
+ * startByte, which is not a reliable per-symbol identity: a language whose
41
+ * extractor merges a synthetic whole-file symbol with independently-derived
42
+ * symbols (e.g. YAML's fallback-root synthetic symbol alongside Helm's
43
+ * regex-derived `define` blocks) can produce two distinct symbols that both
44
+ * start at byte 0. Indexing by array position is unique by construction,
45
+ * regardless of byte overlap — the caller must consume the exact same
46
+ * `parsed.symbols` array (or an equivalently-ordered copy) to look up a
47
+ * symbol by the index this function returns. The value maps callee name ->
48
+ * `memberOnly` (true iff every occurrence was a `x.foo()` member call, never
49
+ * a direct `foo()`), so the indexer can resolve member calls conservatively. */
50
+ export declare function attributeCalls(parsed: ParsedFile): Map<number, Map<string, boolean>>;
51
+ /** Map static semantic relationships (extends/implements/trait use) to the
52
+ * innermost enclosing declaration using the same stable symbol-index identity
53
+ * as call attribution. */
54
+ export declare function attributeRelations(parsed: ParsedFile): Map<number, ParsedRelation[]>;
@@ -0,0 +1,26 @@
1
+ export interface PhpPsr4Mapping {
2
+ prefix: string;
3
+ directories: string[];
4
+ }
5
+ export interface PhpUseImport {
6
+ fqn: string;
7
+ alias: string;
8
+ }
9
+ /** Extract bounded, repository-relative PSR-4 mappings from composer.json.
10
+ * autoload-dev is included because test symbols and changed-file history are
11
+ * first-class graph inputs. Unsafe/absolute mapping roots are ignored. */
12
+ export declare function composerPsr4Mappings(value: unknown): PhpPsr4Mapping[];
13
+ /** Parse ordinary, aliased, comma-separated, function/const, and grouped PHP
14
+ * namespace-use declarations. Dynamic namespace expressions do not exist in
15
+ * this syntax, so every returned import is exact. */
16
+ export declare function parsePhpUseDeclaration(raw: string): PhpUseImport[];
17
+ export interface PhpResolvedReference {
18
+ symbolName: string;
19
+ files: string[];
20
+ }
21
+ export declare function resolvePhpReference(reference: string, namespace: string | null, useDeclarations: readonly string[], mappings: readonly PhpPsr4Mapping[], files: ReadonlySet<string>): PhpResolvedReference;
22
+ /** Resolve one parser import capture. A namespace-use declaration can map to
23
+ * multiple local files; object/static class references map through aliases and
24
+ * current namespace; static include/require paths stay fail-closed. */
25
+ export declare function resolvePhpImportTargets(fromFile: string, capture: string, namespace: string | null, useDeclarations: readonly string[], mappings: readonly PhpPsr4Mapping[], files: ReadonlySet<string>): string[];
26
+ export declare function phpExternalSpecifier(capture: string): string | null;