@loopover/engine 1.0.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/CHANGELOG.md +94 -0
- package/LICENSE +661 -0
- package/README.md +676 -0
- package/dist/advisory/gate-advisory.d.ts +151 -0
- package/dist/advisory/gate-advisory.js +512 -0
- package/dist/ai-policy-map.d.ts +97 -0
- package/dist/ai-policy-map.js +377 -0
- package/dist/ams-policy-spec.d.ts +56 -0
- package/dist/ams-policy-spec.js +165 -0
- package/dist/calibration-dashboard.d.ts +23 -0
- package/dist/calibration-dashboard.js +65 -0
- package/dist/calibration-trend.d.ts +40 -0
- package/dist/calibration-trend.js +60 -0
- package/dist/contributor-fit.d.ts +24 -0
- package/dist/contributor-fit.js +50 -0
- package/dist/discovery-index-contract.d.ts +74 -0
- package/dist/discovery-index-contract.js +184 -0
- package/dist/discovery-soft-claim.d.ts +35 -0
- package/dist/discovery-soft-claim.js +50 -0
- package/dist/duplicate-winner.d.ts +57 -0
- package/dist/duplicate-winner.js +98 -0
- package/dist/feasibility.d.ts +27 -0
- package/dist/feasibility.js +86 -0
- package/dist/finding-severity-calibration.d.ts +116 -0
- package/dist/finding-severity-calibration.js +457 -0
- package/dist/fleet-run-manifest.d.ts +41 -0
- package/dist/fleet-run-manifest.js +151 -0
- package/dist/focus-manifest/guidance.d.ts +21 -0
- package/dist/focus-manifest/guidance.js +222 -0
- package/dist/focus-manifest.d.ts +826 -0
- package/dist/focus-manifest.js +2730 -0
- package/dist/gate-verdict-calibration.d.ts +111 -0
- package/dist/gate-verdict-calibration.js +411 -0
- package/dist/github/constants.d.ts +1 -0
- package/dist/github/constants.js +1 -0
- package/dist/github/sanitize-public-comment.d.ts +1 -0
- package/dist/github/sanitize-public-comment.js +26 -0
- package/dist/goal-model.d.ts +7 -0
- package/dist/goal-model.js +87 -0
- package/dist/governor/action-mode.d.ts +55 -0
- package/dist/governor/action-mode.js +74 -0
- package/dist/governor/budget-cap.d.ts +49 -0
- package/dist/governor/budget-cap.js +34 -0
- package/dist/governor/chokepoint.d.ts +58 -0
- package/dist/governor/chokepoint.js +269 -0
- package/dist/governor/kill-switch.d.ts +38 -0
- package/dist/governor/kill-switch.js +62 -0
- package/dist/governor/rate-limit.d.ts +40 -0
- package/dist/governor/rate-limit.js +63 -0
- package/dist/governor/reputation-throttle.d.ts +50 -0
- package/dist/governor/reputation-throttle.js +94 -0
- package/dist/governor/run-halt.d.ts +39 -0
- package/dist/governor/run-halt.js +83 -0
- package/dist/governor/self-plagiarism.d.ts +47 -0
- package/dist/governor/self-plagiarism.js +200 -0
- package/dist/governor/write-rate-limit.d.ts +52 -0
- package/dist/governor/write-rate-limit.js +132 -0
- package/dist/governor-ledger.d.ts +25 -0
- package/dist/governor-ledger.js +91 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +120 -0
- package/dist/issue-plan-decomposition.d.ts +18 -0
- package/dist/issue-plan-decomposition.js +56 -0
- package/dist/issue-rag-query.d.ts +12 -0
- package/dist/issue-rag-query.js +35 -0
- package/dist/local-scorer.d.ts +34 -0
- package/dist/local-scorer.js +36 -0
- package/dist/metadata-best-min-score.d.ts +9 -0
- package/dist/metadata-best-min-score.js +9 -0
- package/dist/metadata-best-pick.d.ts +9 -0
- package/dist/metadata-best-pick.js +9 -0
- package/dist/metadata-min-score.d.ts +9 -0
- package/dist/metadata-min-score.js +13 -0
- package/dist/metadata-top-min-score.d.ts +9 -0
- package/dist/metadata-top-min-score.js +13 -0
- package/dist/metadata-top-pick.d.ts +9 -0
- package/dist/metadata-top-pick.js +13 -0
- package/dist/miner/acceptance-criteria.d.ts +43 -0
- package/dist/miner/acceptance-criteria.js +77 -0
- package/dist/miner/agent-sdk-driver.d.ts +39 -0
- package/dist/miner/agent-sdk-driver.js +162 -0
- package/dist/miner/attempt-log.d.ts +29 -0
- package/dist/miner/attempt-log.js +100 -0
- package/dist/miner/attempt-metering.d.ts +39 -0
- package/dist/miner/attempt-metering.js +67 -0
- package/dist/miner/cli-subprocess-driver.d.ts +68 -0
- package/dist/miner/cli-subprocess-driver.js +272 -0
- package/dist/miner/coding-agent-driver.d.ts +33 -0
- package/dist/miner/coding-agent-driver.js +36 -0
- package/dist/miner/coding-agent-invoke.d.ts +11 -0
- package/dist/miner/coding-agent-invoke.js +80 -0
- package/dist/miner/coding-agent-mode.d.ts +25 -0
- package/dist/miner/coding-agent-mode.js +43 -0
- package/dist/miner/driver-factory.d.ts +82 -0
- package/dist/miner/driver-factory.js +169 -0
- package/dist/miner/harness-submission-trigger.d.ts +37 -0
- package/dist/miner/harness-submission-trigger.js +46 -0
- package/dist/miner/iterate-loop.d.ts +97 -0
- package/dist/miner/iterate-loop.js +278 -0
- package/dist/miner/iterate-policy.d.ts +98 -0
- package/dist/miner/iterate-policy.js +98 -0
- package/dist/miner/lint-guard.d.ts +50 -0
- package/dist/miner/lint-guard.js +68 -0
- package/dist/miner/local-write-tools.d.ts +81 -0
- package/dist/miner/local-write-tools.js +113 -0
- package/dist/miner/loop-reentry-policy.d.ts +36 -0
- package/dist/miner/loop-reentry-policy.js +53 -0
- package/dist/miner/repo-map.d.ts +51 -0
- package/dist/miner/repo-map.js +268 -0
- package/dist/miner/self-review-adapter.d.ts +106 -0
- package/dist/miner/self-review-adapter.js +83 -0
- package/dist/miner/submission-gate.d.ts +40 -0
- package/dist/miner/submission-gate.js +89 -0
- package/dist/miner/worktree-allocator.d.ts +59 -0
- package/dist/miner/worktree-allocator.js +60 -0
- package/dist/miner/worktree-pool.d.ts +55 -0
- package/dist/miner/worktree-pool.js +65 -0
- package/dist/miner-goal-lane-fit.d.ts +18 -0
- package/dist/miner-goal-lane-fit.js +76 -0
- package/dist/miner-goal-spec.d.ts +139 -0
- package/dist/miner-goal-spec.js +281 -0
- package/dist/miner-prediction-metrics.d.ts +16 -0
- package/dist/miner-prediction-metrics.js +61 -0
- package/dist/miner-telemetry.d.ts +32 -0
- package/dist/miner-telemetry.js +87 -0
- package/dist/objective-anchor.d.ts +125 -0
- package/dist/objective-anchor.js +401 -0
- package/dist/opportunity-competition.d.ts +6 -0
- package/dist/opportunity-competition.js +29 -0
- package/dist/opportunity-freshness.d.ts +18 -0
- package/dist/opportunity-freshness.js +55 -0
- package/dist/opportunity-metadata.d.ts +53 -0
- package/dist/opportunity-metadata.js +204 -0
- package/dist/opportunity-ranker.d.ts +46 -0
- package/dist/opportunity-ranker.js +74 -0
- package/dist/pairwise-calibration.d.ts +49 -0
- package/dist/pairwise-calibration.js +106 -0
- package/dist/phase7-calibration-loop.d.ts +136 -0
- package/dist/phase7-calibration-loop.js +437 -0
- package/dist/plan-blocked.d.ts +6 -0
- package/dist/plan-blocked.js +24 -0
- package/dist/plan-completed.d.ts +5 -0
- package/dist/plan-completed.js +6 -0
- package/dist/plan-completion.d.ts +6 -0
- package/dist/plan-completion.js +7 -0
- package/dist/plan-empty.d.ts +5 -0
- package/dist/plan-empty.js +6 -0
- package/dist/plan-export.d.ts +27 -0
- package/dist/plan-export.js +86 -0
- package/dist/plan-failure.d.ts +5 -0
- package/dist/plan-failure.js +6 -0
- package/dist/plan-overall-status.d.ts +6 -0
- package/dist/plan-overall-status.js +25 -0
- package/dist/plan-pending.d.ts +5 -0
- package/dist/plan-pending.js +6 -0
- package/dist/plan-progress-complete.d.ts +7 -0
- package/dist/plan-progress-complete.js +10 -0
- package/dist/plan-ready.d.ts +6 -0
- package/dist/plan-ready.js +12 -0
- package/dist/plan-running.d.ts +5 -0
- package/dist/plan-running.js +6 -0
- package/dist/plan-skipped.d.ts +5 -0
- package/dist/plan-skipped.js +6 -0
- package/dist/plan-step-count.d.ts +5 -0
- package/dist/plan-step-count.js +6 -0
- package/dist/plan-step-stats.d.ts +5 -0
- package/dist/plan-step-stats.js +6 -0
- package/dist/plan-templates.d.ts +25 -0
- package/dist/plan-templates.js +107 -0
- package/dist/plan-terminated.d.ts +6 -0
- package/dist/plan-terminated.js +9 -0
- package/dist/portfolio/non-convergence.d.ts +37 -0
- package/dist/portfolio/non-convergence.js +57 -0
- package/dist/portfolio/queue.d.ts +30 -0
- package/dist/portfolio/queue.js +150 -0
- package/dist/predicted-gate.d.ts +103 -0
- package/dist/predicted-gate.js +218 -0
- package/dist/prompt-packet.d.ts +11 -0
- package/dist/prompt-packet.js +34 -0
- package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-best-min-score.js +9 -0
- package/dist/ranked-opportunity-best-pick.d.ts +8 -0
- package/dist/ranked-opportunity-best-pick.js +9 -0
- package/dist/ranked-opportunity-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-min-score.js +13 -0
- package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-top-min-score.js +13 -0
- package/dist/review/advisory-ai-routing-config.d.ts +8 -0
- package/dist/review/advisory-ai-routing-config.js +40 -0
- package/dist/review/check-names.d.ts +1 -0
- package/dist/review/check-names.js +1 -0
- package/dist/review/cla-check.d.ts +49 -0
- package/dist/review/cla-check.js +71 -0
- package/dist/review/diff-file-priority.d.ts +1 -0
- package/dist/review/diff-file-priority.js +12 -0
- package/dist/review/enrichment-analyzer-names.d.ts +3 -0
- package/dist/review/enrichment-analyzer-names.js +63 -0
- package/dist/review/guardrail-config.d.ts +10 -0
- package/dist/review/guardrail-config.js +62 -0
- package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
- package/dist/review/linked-issue-hard-rules-config.js +79 -0
- package/dist/review/linked-issue-label-propagation.d.ts +9 -0
- package/dist/review/linked-issue-label-propagation.js +114 -0
- package/dist/review/pre-merge-checks.d.ts +28 -0
- package/dist/review/pre-merge-checks.js +66 -0
- package/dist/review/review-thread-findings.d.ts +1 -0
- package/dist/review/review-thread-findings.js +1 -0
- package/dist/review/safe-url.d.ts +5 -0
- package/dist/review/safe-url.js +137 -0
- package/dist/review/screenshot-table-gate.d.ts +87 -0
- package/dist/review/screenshot-table-gate.js +340 -0
- package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
- package/dist/review/unlinked-issue-guardrail-config.js +44 -0
- package/dist/reviewer-consensus-calibration.d.ts +113 -0
- package/dist/reviewer-consensus-calibration.js +493 -0
- package/dist/reward-risk.d.ts +223 -0
- package/dist/reward-risk.js +618 -0
- package/dist/scoring/label-match.d.ts +4 -0
- package/dist/scoring/label-match.js +145 -0
- package/dist/scoring/model.d.ts +31 -0
- package/dist/scoring/model.js +157 -0
- package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
- package/dist/scoring/pending-pr-scenarios.js +145 -0
- package/dist/scoring/preview.d.ts +246 -0
- package/dist/scoring/preview.js +1108 -0
- package/dist/scoring/types.d.ts +131 -0
- package/dist/scoring/types.js +7 -0
- package/dist/settings/auto-close-exempt.d.ts +10 -0
- package/dist/settings/auto-close-exempt.js +58 -0
- package/dist/settings/autonomy.d.ts +33 -0
- package/dist/settings/autonomy.js +80 -0
- package/dist/settings/command-authorization.d.ts +49 -0
- package/dist/settings/command-authorization.js +222 -0
- package/dist/settings/contributor-blacklist.d.ts +17 -0
- package/dist/settings/contributor-blacklist.js +86 -0
- package/dist/settings/moderation-rules.d.ts +51 -0
- package/dist/settings/moderation-rules.js +104 -0
- package/dist/settings/pr-type-label.d.ts +70 -0
- package/dist/settings/pr-type-label.js +149 -0
- package/dist/signals/change-guardrail.d.ts +49 -0
- package/dist/signals/change-guardrail.js +164 -0
- package/dist/signals/check-summary.d.ts +9 -0
- package/dist/signals/check-summary.js +12 -0
- package/dist/signals/contributor-calibration.d.ts +23 -0
- package/dist/signals/contributor-calibration.js +47 -0
- package/dist/signals/duplicate-winner.d.ts +9 -0
- package/dist/signals/duplicate-winner.js +9 -0
- package/dist/signals/path-matchers.d.ts +36 -0
- package/dist/signals/path-matchers.js +371 -0
- package/dist/signals/predicted-gate-engine.d.ts +62 -0
- package/dist/signals/predicted-gate-engine.js +895 -0
- package/dist/signals/preflight-limits.d.ts +14 -0
- package/dist/signals/preflight-limits.js +14 -0
- package/dist/signals/slop.d.ts +56 -0
- package/dist/signals/slop.js +324 -0
- package/dist/signals/test-evidence.d.ts +25 -0
- package/dist/signals/test-evidence.js +108 -0
- package/dist/subprocess-env.d.ts +23 -0
- package/dist/subprocess-env.js +74 -0
- package/dist/track-record-summary.d.ts +92 -0
- package/dist/track-record-summary.js +301 -0
- package/dist/types/manifest-deps-types.d.ts +514 -0
- package/dist/types/manifest-deps-types.js +4 -0
- package/dist/types/predicted-gate-types.d.ts +338 -0
- package/dist/types/predicted-gate-types.js +3 -0
- package/dist/types/reward-risk-types.d.ts +319 -0
- package/dist/types/reward-risk-types.js +11 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +80 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** A minimal, self-contained JSON-value type (mirrors src/types.ts's own JsonValue) -- kept local rather than
|
|
2
|
+
* imported so this module has zero cross-package type dependency beyond what it already needs. */
|
|
3
|
+
export type LocalWriteJsonValue = string | number | boolean | null | LocalWriteJsonValue[] | {
|
|
4
|
+
[key: string]: LocalWriteJsonValue;
|
|
5
|
+
};
|
|
6
|
+
export declare const LOCAL_WRITE_BOUNDARY = "Run this locally with your OWN GitHub credentials (e.g. an authenticated `gh`/`git`). Gittensory supplies the content but never performs the write \u2014 your code and the action both stay on your machine.";
|
|
7
|
+
export type LocalWriteActionSpec = {
|
|
8
|
+
action: string;
|
|
9
|
+
description: string;
|
|
10
|
+
inputs: Record<string, LocalWriteJsonValue>;
|
|
11
|
+
command: string;
|
|
12
|
+
boundary: string;
|
|
13
|
+
};
|
|
14
|
+
/** Open a PR from a local branch (content typically taken from gittensory's prepare_pr_packet). */
|
|
15
|
+
export declare function buildOpenPrSpec(input: {
|
|
16
|
+
repoFullName: string;
|
|
17
|
+
base: string;
|
|
18
|
+
head: string;
|
|
19
|
+
title: string;
|
|
20
|
+
body: string;
|
|
21
|
+
draft?: boolean | undefined;
|
|
22
|
+
}): LocalWriteActionSpec;
|
|
23
|
+
/** Close a pull request the miner itself opened (e.g. it lost a claim-conflict adjudication to an earlier
|
|
24
|
+
* claimant, #4848) -- never used against a PR the miner does not own. The close runs FIRST and
|
|
25
|
+
* unconditionally -- it's the safety-critical action (never leave a known-losing PR open); `comment`, when
|
|
26
|
+
* supplied, is a best-effort follow-up posted only once the close itself succeeds (`gh pr close` has no
|
|
27
|
+
* comment-body flag of its own, and a transient `gh pr comment` failure must never mask or block the close
|
|
28
|
+
* it's explaining). */
|
|
29
|
+
export declare function buildClosePrSpec(input: {
|
|
30
|
+
repoFullName: string;
|
|
31
|
+
number: number;
|
|
32
|
+
comment?: string | undefined;
|
|
33
|
+
}): LocalWriteActionSpec;
|
|
34
|
+
/** File an issue (e.g. an issue-discovery proposal). */
|
|
35
|
+
export declare function buildFileIssueSpec(input: {
|
|
36
|
+
repoFullName: string;
|
|
37
|
+
title: string;
|
|
38
|
+
body: string;
|
|
39
|
+
labels?: string[] | undefined;
|
|
40
|
+
}): LocalWriteActionSpec;
|
|
41
|
+
/** Add labels to an issue or PR (gh issue edit also targets PRs). */
|
|
42
|
+
export declare function buildApplyLabelsSpec(input: {
|
|
43
|
+
repoFullName: string;
|
|
44
|
+
number: number;
|
|
45
|
+
labels: string[];
|
|
46
|
+
}): LocalWriteActionSpec;
|
|
47
|
+
/** Post an eligibility/context comment on an issue or PR. */
|
|
48
|
+
export declare function buildPostEligibilityCommentSpec(input: {
|
|
49
|
+
repoFullName: string;
|
|
50
|
+
number: number;
|
|
51
|
+
body: string;
|
|
52
|
+
}): LocalWriteActionSpec;
|
|
53
|
+
/** Create a local branch off an optional base. */
|
|
54
|
+
export declare function buildCreateBranchSpec(input: {
|
|
55
|
+
branch: string;
|
|
56
|
+
base?: string | undefined;
|
|
57
|
+
}): LocalWriteActionSpec;
|
|
58
|
+
/** Delete a branch locally, and optionally on the remote. */
|
|
59
|
+
export declare function buildDeleteBranchSpec(input: {
|
|
60
|
+
branch: string;
|
|
61
|
+
remote?: boolean | undefined;
|
|
62
|
+
}): LocalWriteActionSpec;
|
|
63
|
+
export declare function buildTestGenSpec(input: {
|
|
64
|
+
repoFullName: string;
|
|
65
|
+
targetFiles: string[];
|
|
66
|
+
framework: string;
|
|
67
|
+
testDir?: string | null | undefined;
|
|
68
|
+
criteria?: string[] | undefined;
|
|
69
|
+
}): LocalWriteActionSpec;
|
|
70
|
+
/** Build a LOCAL-execution spec to file a follow-up issue for a review finding a maintainer wants TRACKED
|
|
71
|
+
* rather than blocked on this PR. Composes a bounded, public-safe title/body from the finding and delegates to
|
|
72
|
+
* {@link buildFileIssueSpec}'s exact "file_issue" spec shape — no new write path. `label` is optional: when the
|
|
73
|
+
* caller supplies a point-bearing label (e.g. "gittensor:bug"), the follow-up carries it so the tracked issue
|
|
74
|
+
* is itself a scored, actionable contribution target; omitted ⇒ no labels at all (empty-label branch). */
|
|
75
|
+
export declare function buildFollowUpIssueSpec(input: {
|
|
76
|
+
repoFullName: string;
|
|
77
|
+
path: string;
|
|
78
|
+
line?: number | undefined;
|
|
79
|
+
finding: string;
|
|
80
|
+
label?: string | null | undefined;
|
|
81
|
+
}): LocalWriteActionSpec;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// #780 miner write-tools. These build ACTION SPECS — gittensory supplies the content; the miner's OWN local
|
|
2
|
+
// harness runs the command with its OWN GitHub credentials. Gittensory NEVER performs the write, so source code
|
|
3
|
+
// and the write both stay on the miner's machine: the no-cloud-write boundary holds. Pure + deterministic: every
|
|
4
|
+
// builder returns a self-contained, shell-safe spec and touches nothing.
|
|
5
|
+
//
|
|
6
|
+
// MOVED HERE FROM src/mcp/local-write-tools.ts (#2337): this module has zero root-specific dependencies (it only
|
|
7
|
+
// ever needed a generic JSON-value type), so it belongs in the shared "brain" layer alongside the rest of the
|
|
8
|
+
// portable engine, not root-only. This is what lets packages/gittensory-miner's own real driving-loop entrypoint
|
|
9
|
+
// construct the EXACT SAME open_pr command gittensory's MCP server would return, with zero network round-trip
|
|
10
|
+
// and zero duplicated/drifting logic: both consumers import the same functions from this one place. Root's
|
|
11
|
+
// src/mcp/local-write-tools.ts is now a thin re-export preserving every existing import path unchanged.
|
|
12
|
+
export const LOCAL_WRITE_BOUNDARY = "Run this locally with your OWN GitHub credentials (e.g. an authenticated `gh`/`git`). Gittensory supplies the content but never performs the write — your code and the action both stay on your machine.";
|
|
13
|
+
// POSIX single-quote escaping: wrap in single quotes and escape embedded single quotes. Safe against injection
|
|
14
|
+
// when the harness runs `command` verbatim.
|
|
15
|
+
function sq(value) {
|
|
16
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
17
|
+
}
|
|
18
|
+
function spec(action, description, inputs, command) {
|
|
19
|
+
return { action, description, inputs, command, boundary: LOCAL_WRITE_BOUNDARY };
|
|
20
|
+
}
|
|
21
|
+
/** Open a PR from a local branch (content typically taken from gittensory's prepare_pr_packet). */
|
|
22
|
+
export function buildOpenPrSpec(input) {
|
|
23
|
+
const draft = input.draft === true;
|
|
24
|
+
const command = `gh pr create --repo ${sq(input.repoFullName)} --base ${sq(input.base)} --head ${sq(input.head)} --title ${sq(input.title)} --body ${sq(input.body)}${draft ? " --draft" : ""}`;
|
|
25
|
+
return spec("open_pr", "Open a pull request from your local branch.", { repoFullName: input.repoFullName, base: input.base, head: input.head, title: input.title, body: input.body, draft }, command);
|
|
26
|
+
}
|
|
27
|
+
/** Close a pull request the miner itself opened (e.g. it lost a claim-conflict adjudication to an earlier
|
|
28
|
+
* claimant, #4848) -- never used against a PR the miner does not own. The close runs FIRST and
|
|
29
|
+
* unconditionally -- it's the safety-critical action (never leave a known-losing PR open); `comment`, when
|
|
30
|
+
* supplied, is a best-effort follow-up posted only once the close itself succeeds (`gh pr close` has no
|
|
31
|
+
* comment-body flag of its own, and a transient `gh pr comment` failure must never mask or block the close
|
|
32
|
+
* it's explaining). */
|
|
33
|
+
export function buildClosePrSpec(input) {
|
|
34
|
+
const closeCommand = `gh pr close ${input.number} --repo ${sq(input.repoFullName)}`;
|
|
35
|
+
const command = input.comment
|
|
36
|
+
? `${closeCommand} && gh pr comment ${input.number} --repo ${sq(input.repoFullName)} --body ${sq(input.comment)}`
|
|
37
|
+
: closeCommand;
|
|
38
|
+
return spec("close_pr", "Close a pull request you opened.", { repoFullName: input.repoFullName, number: input.number, ...(input.comment ? { comment: input.comment } : {}) }, command);
|
|
39
|
+
}
|
|
40
|
+
/** File an issue (e.g. an issue-discovery proposal). */
|
|
41
|
+
export function buildFileIssueSpec(input) {
|
|
42
|
+
const labels = input.labels ?? [];
|
|
43
|
+
const labelArgs = labels.map((label) => ` --label ${sq(label)}`).join("");
|
|
44
|
+
const command = `gh issue create --repo ${sq(input.repoFullName)} --title ${sq(input.title)} --body ${sq(input.body)}${labelArgs}`;
|
|
45
|
+
return spec("file_issue", "File a new issue.", { repoFullName: input.repoFullName, title: input.title, body: input.body, labels }, command);
|
|
46
|
+
}
|
|
47
|
+
/** Add labels to an issue or PR (gh issue edit also targets PRs). */
|
|
48
|
+
export function buildApplyLabelsSpec(input) {
|
|
49
|
+
const labelArgs = input.labels.map((label) => ` --add-label ${sq(label)}`).join("");
|
|
50
|
+
const command = `gh issue edit ${input.number} --repo ${sq(input.repoFullName)}${labelArgs}`;
|
|
51
|
+
return spec("apply_labels", "Add labels to an issue or pull request.", { repoFullName: input.repoFullName, number: input.number, labels: input.labels }, command);
|
|
52
|
+
}
|
|
53
|
+
/** Post an eligibility/context comment on an issue or PR. */
|
|
54
|
+
export function buildPostEligibilityCommentSpec(input) {
|
|
55
|
+
const command = `gh issue comment ${input.number} --repo ${sq(input.repoFullName)} --body ${sq(input.body)}`;
|
|
56
|
+
return spec("post_eligibility_comment", "Post an eligibility/context comment on an issue or pull request.", { repoFullName: input.repoFullName, number: input.number, body: input.body }, command);
|
|
57
|
+
}
|
|
58
|
+
/** Create a local branch off an optional base. */
|
|
59
|
+
export function buildCreateBranchSpec(input) {
|
|
60
|
+
const command = input.base ? `git switch -c ${sq(input.branch)} ${sq(input.base)}` : `git switch -c ${sq(input.branch)}`;
|
|
61
|
+
return spec("create_branch", "Create a local branch.", { branch: input.branch, ...(input.base ? { base: input.base } : {}) }, command);
|
|
62
|
+
}
|
|
63
|
+
/** Delete a branch locally, and optionally on the remote. */
|
|
64
|
+
export function buildDeleteBranchSpec(input) {
|
|
65
|
+
const local = `git branch -D ${sq(input.branch)}`;
|
|
66
|
+
const command = input.remote === true ? `${local} && git push origin --delete ${sq(input.branch)}` : local;
|
|
67
|
+
return spec("delete_branch", "Delete a branch (locally, and optionally on origin).", { branch: input.branch, remote: input.remote === true }, command);
|
|
68
|
+
}
|
|
69
|
+
// #2188 (boundary-safe test-generation slice of #1972). Unlike the write-tools above, there is no single CLI
|
|
70
|
+
// verb that "scaffolds a test file" across vitest/jest/pytest/go test/rspec/cargo test — so `command` here is a
|
|
71
|
+
// safe, informative `echo` of the plan (target files + boundary criteria) rather than a real write, and the
|
|
72
|
+
// actual scaffolding is left to the contributor's OWN agent reading the structured `inputs`. This keeps the same
|
|
73
|
+
// no-cloud-write guarantee as every other spec in this file: gittensory supplies WHAT test cases should exist at
|
|
74
|
+
// which boundaries, never the test file content or its execution.
|
|
75
|
+
export function buildTestGenSpec(input) {
|
|
76
|
+
const criteria = input.criteria ?? [];
|
|
77
|
+
const testDir = input.testDir ?? null;
|
|
78
|
+
const targetList = input.targetFiles.join(", ");
|
|
79
|
+
const criteriaList = criteria.length > 0 ? ` Boundary-safe criteria: ${criteria.join("; ")}.` : "";
|
|
80
|
+
const location = testDir ? ` under ${testDir}` : " co-located with the source it covers";
|
|
81
|
+
const description = `Scaffold ${input.framework} tests${location} for: ${targetList}.${criteriaList}`;
|
|
82
|
+
const command = `echo ${sq(description)}`;
|
|
83
|
+
return spec("generate_tests", description, { repoFullName: input.repoFullName, targetFiles: input.targetFiles, framework: input.framework, testDir, criteria }, command);
|
|
84
|
+
}
|
|
85
|
+
// #2177 (follow-up-issue slice of #1962). Reuses buildFileIssueSpec's exact spec shape ("file_issue") — a
|
|
86
|
+
// deferred review finding is just another issue-worth-filing content source, so there is no new spec verb or
|
|
87
|
+
// no-cloud-write boundary here, only a deterministic title/body composer in front of the SAME builder.
|
|
88
|
+
const FOLLOW_UP_ISSUE_TITLE_MAX = 200;
|
|
89
|
+
const FOLLOW_UP_ISSUE_BODY_MAX = 4000;
|
|
90
|
+
// Strip any machine-readable marker (e.g. fix-handoff's HTML comment marker, or a stray fenced block) before
|
|
91
|
+
// the finding's text becomes issue content — a follow-up issue is read by a HUMAN triaging a backlog, not a
|
|
92
|
+
// harness, so it should read as prose, not carry an internal marker meant for a different consumer.
|
|
93
|
+
function stripMachineMarkers(text) {
|
|
94
|
+
return text
|
|
95
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
96
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
97
|
+
.replace(/\s+/g, " ")
|
|
98
|
+
.trim();
|
|
99
|
+
}
|
|
100
|
+
/** Build a LOCAL-execution spec to file a follow-up issue for a review finding a maintainer wants TRACKED
|
|
101
|
+
* rather than blocked on this PR. Composes a bounded, public-safe title/body from the finding and delegates to
|
|
102
|
+
* {@link buildFileIssueSpec}'s exact "file_issue" spec shape — no new write path. `label` is optional: when the
|
|
103
|
+
* caller supplies a point-bearing label (e.g. "gittensor:bug"), the follow-up carries it so the tracked issue
|
|
104
|
+
* is itself a scored, actionable contribution target; omitted ⇒ no labels at all (empty-label branch). */
|
|
105
|
+
export function buildFollowUpIssueSpec(input) {
|
|
106
|
+
const safePath = stripMachineMarkers(input.path);
|
|
107
|
+
const location = input.line && input.line > 0 ? `${safePath}:${input.line}` : safePath;
|
|
108
|
+
const safeFinding = stripMachineMarkers(input.finding).slice(0, FOLLOW_UP_ISSUE_BODY_MAX);
|
|
109
|
+
const title = `Follow up: ${location}`.slice(0, FOLLOW_UP_ISSUE_TITLE_MAX);
|
|
110
|
+
const body = `Deferred review finding at \`${location}\`:\n\n${safeFinding}`.slice(0, FOLLOW_UP_ISSUE_BODY_MAX);
|
|
111
|
+
const labels = input.label ? [input.label] : [];
|
|
112
|
+
return buildFileIssueSpec({ repoFullName: input.repoFullName, title, body, labels });
|
|
113
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type MinerKillSwitchScope } from "../governor/kill-switch.js";
|
|
2
|
+
/** The terminal outcome that just resolved for the repo the caller is considering re-entering on. */
|
|
3
|
+
export type LoopReentryOutcome = "merged" | "disengaged" | "other";
|
|
4
|
+
export declare const DEFAULT_MAX_CONSECUTIVE_DISENGAGEMENTS = 3;
|
|
5
|
+
export declare const DEFAULT_MAX_REENTRIES_PER_HOUR = 4;
|
|
6
|
+
export declare const DEFAULT_MAX_REENTRIES_PER_SESSION = 20;
|
|
7
|
+
export type LoopReentryCandidate = {
|
|
8
|
+
/** Checked FIRST, before any other field below -- see the module doc comment's KILL-SWITCH section. */
|
|
9
|
+
killSwitchScope: MinerKillSwitchScope;
|
|
10
|
+
repoFullName: string;
|
|
11
|
+
outcome: LoopReentryOutcome;
|
|
12
|
+
/** Caller-computed count of CONSECUTIVE `"disengaged"` outcomes for this repo, ending with (and including,
|
|
13
|
+
* when `outcome === "disengaged"`) this one. Any non-disengaged outcome resets this to 0 -- the caller owns
|
|
14
|
+
* that computation, this policy only consumes the resulting integer (mirrors `reputation-throttle.ts`'s
|
|
15
|
+
* caller-supplied `RepoOutcomeHistory`). */
|
|
16
|
+
consecutiveDisengagements: number;
|
|
17
|
+
maxConsecutiveDisengagements?: number | undefined;
|
|
18
|
+
/** Caller-tracked re-entry counters for the hard rate/session cap -- independent of the per-repo circuit
|
|
19
|
+
* breaker above. */
|
|
20
|
+
reentriesThisHour: number;
|
|
21
|
+
maxReentriesPerHour?: number | undefined;
|
|
22
|
+
reentriesThisSession: number;
|
|
23
|
+
maxReentriesPerSession?: number | undefined;
|
|
24
|
+
};
|
|
25
|
+
export type LoopReentryDecision = {
|
|
26
|
+
reenter: boolean;
|
|
27
|
+
/** Always populated when `reenter` is `false`; every ceiling that was hit, not just the first. */
|
|
28
|
+
reasons: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Decide whether the loop may re-enter discovery for this repo. Pure; identical inputs always yield the
|
|
32
|
+
* identical decision. `outcome === "merged"` alone never bypasses the rate/session cap -- a healthy repo can
|
|
33
|
+
* still be rate-limited if the operator-wide ceiling is already spent. The kill-switch is checked FIRST and
|
|
34
|
+
* short-circuits everything else -- an active kill-switch blocks unconditionally.
|
|
35
|
+
*/
|
|
36
|
+
export declare function shouldReenter(candidate: LoopReentryCandidate): LoopReentryDecision;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Closed-loop discovery re-entry policy (#2338): the pure decision half of "on a resolved outcome (merged, or
|
|
2
|
+
// rejected-and-disengaged), automatically re-invoke discovery to select the next candidate." Deliberately split
|
|
3
|
+
// from the miner-side orchestrator (packages/gittensory-miner/lib/loop-reentry.js), which owns the REAL IO --
|
|
4
|
+
// reading recent event-ledger history to compute the tallies this policy consumes, dequeuing the next
|
|
5
|
+
// candidate, and transitioning run-state -- mirroring this session's established engine (pure) / miner-lib
|
|
6
|
+
// (stateful) split for every other governor primitive.
|
|
7
|
+
//
|
|
8
|
+
// TOP SLOP-AT-SCALE RISK: this issue's own framing calls out "a bug here (re-entering too fast, ignoring a
|
|
9
|
+
// circuit-breaker, or looping on a permanently-rejected repo) is the top slop-at-scale risk for the whole miner
|
|
10
|
+
// subsystem." Both failure modes get an INDEPENDENT hard ceiling here, neither one masking the other:
|
|
11
|
+
// - A per-repo circuit breaker: N consecutive disengaged (rejected) outcomes on the SAME repo pauses further
|
|
12
|
+
// re-entry for that repo, regardless of how much of the hour/session rate budget remains.
|
|
13
|
+
// - A hard rate/session cap: independent of any repo's own history, a conservative ceiling on how many
|
|
14
|
+
// re-entries may fire in a rolling hour or across the whole session.
|
|
15
|
+
// Both reasons are collected (not short-circuited) so a caller logging the decision sees every ceiling that
|
|
16
|
+
// was hit, not just the first one checked.
|
|
17
|
+
//
|
|
18
|
+
// KILL-SWITCH (#2339): checked FIRST, before any other logic -- flipping the kill-switch must halt any pending
|
|
19
|
+
// re-entry immediately, the same way it halts the Governor chokepoint (#2340). Reuses
|
|
20
|
+
// `isMinerKillSwitchActive` (kill-switch.ts, #2341) directly -- the identical shared helper
|
|
21
|
+
// `submission-gate.ts`'s `shouldSubmit` consults, per #2339's own "single shared helper, not duplicated per
|
|
22
|
+
// call site" deliverable. Unlike the reasons above, the kill-switch check DOES short-circuit (an active kill-
|
|
23
|
+
// switch is the only reason reported) -- "as their FIRST guard, before any other logic" reads as "don't even
|
|
24
|
+
// evaluate the rest," not "collect this alongside the rest."
|
|
25
|
+
import { isMinerKillSwitchActive } from "../governor/kill-switch.js";
|
|
26
|
+
export const DEFAULT_MAX_CONSECUTIVE_DISENGAGEMENTS = 3;
|
|
27
|
+
export const DEFAULT_MAX_REENTRIES_PER_HOUR = 4;
|
|
28
|
+
export const DEFAULT_MAX_REENTRIES_PER_SESSION = 20;
|
|
29
|
+
/**
|
|
30
|
+
* Decide whether the loop may re-enter discovery for this repo. Pure; identical inputs always yield the
|
|
31
|
+
* identical decision. `outcome === "merged"` alone never bypasses the rate/session cap -- a healthy repo can
|
|
32
|
+
* still be rate-limited if the operator-wide ceiling is already spent. The kill-switch is checked FIRST and
|
|
33
|
+
* short-circuits everything else -- an active kill-switch blocks unconditionally.
|
|
34
|
+
*/
|
|
35
|
+
export function shouldReenter(candidate) {
|
|
36
|
+
if (isMinerKillSwitchActive(candidate.killSwitchScope)) {
|
|
37
|
+
return { reenter: false, reasons: [`${candidate.killSwitchScope}_kill_switch_active`] };
|
|
38
|
+
}
|
|
39
|
+
const reasons = [];
|
|
40
|
+
const maxConsecutiveDisengagements = candidate.maxConsecutiveDisengagements ?? DEFAULT_MAX_CONSECUTIVE_DISENGAGEMENTS;
|
|
41
|
+
const maxReentriesPerHour = candidate.maxReentriesPerHour ?? DEFAULT_MAX_REENTRIES_PER_HOUR;
|
|
42
|
+
const maxReentriesPerSession = candidate.maxReentriesPerSession ?? DEFAULT_MAX_REENTRIES_PER_SESSION;
|
|
43
|
+
if (candidate.outcome === "disengaged" && candidate.consecutiveDisengagements >= maxConsecutiveDisengagements) {
|
|
44
|
+
reasons.push(`repo_paused_after_consecutive_disengagements:${candidate.consecutiveDisengagements}>=${maxConsecutiveDisengagements}`);
|
|
45
|
+
}
|
|
46
|
+
if (candidate.reentriesThisHour >= maxReentriesPerHour) {
|
|
47
|
+
reasons.push(`hourly_reentry_cap_reached:${candidate.reentriesThisHour}>=${maxReentriesPerHour}`);
|
|
48
|
+
}
|
|
49
|
+
if (candidate.reentriesThisSession >= maxReentriesPerSession) {
|
|
50
|
+
reasons.push(`session_reentry_cap_reached:${candidate.reentriesThisSession}>=${maxReentriesPerSession}`);
|
|
51
|
+
}
|
|
52
|
+
return { reenter: reasons.length === 0, reasons };
|
|
53
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Parser from "web-tree-sitter";
|
|
2
|
+
export type RepoMapSymbolKind = "function" | "class" | "method" | "interface" | "type";
|
|
3
|
+
export type RepoMapSymbol = {
|
|
4
|
+
kind: RepoMapSymbolKind;
|
|
5
|
+
name: string;
|
|
6
|
+
signature: string;
|
|
7
|
+
line: number;
|
|
8
|
+
};
|
|
9
|
+
export type RepoMapSkipReason = "unsupported_language" | "grammar_unavailable" | "resource_limit";
|
|
10
|
+
export type RepoMapFileEntry = {
|
|
11
|
+
path: string;
|
|
12
|
+
language: string | null;
|
|
13
|
+
symbols: readonly RepoMapSymbol[];
|
|
14
|
+
skipped?: RepoMapSkipReason;
|
|
15
|
+
};
|
|
16
|
+
export type RepoMapSourceFile = {
|
|
17
|
+
path: string;
|
|
18
|
+
sourceText: string;
|
|
19
|
+
};
|
|
20
|
+
/** Pure: map a file path to the grammar name that would parse it, or null if unsupported. */
|
|
21
|
+
export declare function resolveRepoMapLanguage(path: string): string | null;
|
|
22
|
+
/** Test/injection seam for loading a compiled grammar -- real WASM-file IO lives only in the default
|
|
23
|
+
* implementation, so a test can inject a failing loader to exercise `grammar_unavailable` without needing an
|
|
24
|
+
* actually-broken WASM file. */
|
|
25
|
+
export type LoadRepoMapLanguageFn = (languageName: string) => Promise<Parser.Language>;
|
|
26
|
+
export type ExtractRepoMapSymbolsOptions = {
|
|
27
|
+
maxSignatureChars?: number | undefined;
|
|
28
|
+
maxAstNodes?: number | undefined;
|
|
29
|
+
maxSymbols?: number | undefined;
|
|
30
|
+
};
|
|
31
|
+
/** Walk a parsed tree collecting one `RepoMapSymbol` per matched node kind (function/class/method/interface/
|
|
32
|
+
* type declarations). Pure given an already-parsed tree. Returns `null` when extraction exceeds its work budget. */
|
|
33
|
+
export declare function extractRepoMapSymbols(tree: Parser.Tree, maxSignatureChars?: number): RepoMapSymbol[] | null;
|
|
34
|
+
export declare function extractRepoMapSymbols(tree: Parser.Tree, sourceText: string, options?: ExtractRepoMapSymbolsOptions): RepoMapSymbol[] | null;
|
|
35
|
+
export type BuildRepoMapOptions = {
|
|
36
|
+
loadLanguage?: LoadRepoMapLanguageFn | undefined;
|
|
37
|
+
maxSignatureChars?: number | undefined;
|
|
38
|
+
maxFiles?: number | undefined;
|
|
39
|
+
maxSourceBytes?: number | undefined;
|
|
40
|
+
maxTotalSourceBytes?: number | undefined;
|
|
41
|
+
maxAstNodes?: number | undefined;
|
|
42
|
+
maxSymbols?: number | undefined;
|
|
43
|
+
};
|
|
44
|
+
/** Build one `RepoMapFileEntry` per source file: unsupported extensions and grammar/parse failures are caught
|
|
45
|
+
* and reported via `skipped`, never thrown -- see module header. A language's grammar is only loaded once per
|
|
46
|
+
* call even across many files of the same language. */
|
|
47
|
+
export declare function buildRepoMap(files: readonly RepoMapSourceFile[], options?: BuildRepoMapOptions): Promise<RepoMapFileEntry[]>;
|
|
48
|
+
/** Render entries into a bounded plain-text outline: one line per symbol (`kind name (line N): signature`),
|
|
49
|
+
* skipped/empty files noted with a one-line placeholder. Stops once `maxOutputChars` would be exceeded and
|
|
50
|
+
* appends a truncation marker, so a caller/prompt-builder can tell the map is partial rather than complete. */
|
|
51
|
+
export declare function renderRepoMap(entries: readonly RepoMapFileEntry[], maxOutputChars?: number): string;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Tree-sitter-based repo map builder (#4280): gives a coding-agent driver (or the acceptance-criteria/prompt-
|
|
2
|
+
// packet builders upstream of it) a compact, structural view of a target repository -- function/class/method
|
|
3
|
+
// signatures -- without paying the token cost of dumping full file contents into a prompt. Uses `web-tree-sitter`
|
|
4
|
+
// (the WASM binding) with prebuilt grammars from `tree-sitter-wasms`, not a native addon: this package also ships
|
|
5
|
+
// a Cloudflare Workers deployment target where native Node addons are not an option. This module only ever runs
|
|
6
|
+
// in the local miner/CLI process, but the WASM binding keeps that door open and needs no native build step.
|
|
7
|
+
//
|
|
8
|
+
// Supported today: JavaScript/TypeScript/TSX (this repo's own dominant languages). A file whose extension has no
|
|
9
|
+
// mapped grammar is skipped (not crashed on) with `skipped: "unsupported_language"`; a grammar that fails to load
|
|
10
|
+
// or a parse that throws is caught the same way, with `skipped: "grammar_unavailable"` -- this module's contract
|
|
11
|
+
// is "extract what it safely can," never "block the whole driver invocation."
|
|
12
|
+
//
|
|
13
|
+
// Known scope limit: only `function`/`class` declarations and expressions, `method_definition`, `interface`, and
|
|
14
|
+
// `type` alias nodes are extracted -- an arrow function or class expression bound via a `const foo = ...`
|
|
15
|
+
// declarator is not walked up to its binding identifier, so it is either missed (arrow functions aren't matched
|
|
16
|
+
// at all yet) or reported as "<anonymous>" (a bare class/function expression). Good enough for a compact outline
|
|
17
|
+
// today; resolving binding names is a reasonable follow-up, not attempted here.
|
|
18
|
+
import { Buffer } from "node:buffer";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
import Parser from "web-tree-sitter";
|
|
22
|
+
// Lazy, not module-scope: this file is reachable from the Cloudflare Workers bundle (barrel-exported via
|
|
23
|
+
// `@loopover/engine`) even though nothing there ever calls `buildRepoMap`. `import.meta.url` is
|
|
24
|
+
// undefined in that bundle's startup-validation context, so an eager `createRequire(import.meta.url)` at
|
|
25
|
+
// module scope would crash the Worker's deploy before any request is served. Deferring construction to
|
|
26
|
+
// first real use keeps this module import-safe everywhere while still working for its actual CLI callers.
|
|
27
|
+
let cachedRequire = null;
|
|
28
|
+
function requireFromHere() {
|
|
29
|
+
return (cachedRequire ??= createRequire(import.meta.url));
|
|
30
|
+
}
|
|
31
|
+
/** File extension (including the leading dot) -> tree-sitter grammar name in `tree-sitter-wasms`. */
|
|
32
|
+
const LANGUAGE_BY_EXTENSION = Object.freeze({
|
|
33
|
+
".js": "javascript",
|
|
34
|
+
".mjs": "javascript",
|
|
35
|
+
".cjs": "javascript",
|
|
36
|
+
".jsx": "javascript",
|
|
37
|
+
".ts": "typescript",
|
|
38
|
+
".mts": "typescript",
|
|
39
|
+
".cts": "typescript",
|
|
40
|
+
".tsx": "tsx",
|
|
41
|
+
});
|
|
42
|
+
const SYMBOL_NODE_KIND = Object.freeze({
|
|
43
|
+
function_declaration: "function",
|
|
44
|
+
// `function_expression`/`class` (bare, unnamed) cover `export default function() {}` / `export default class {}`
|
|
45
|
+
// and other expression positions -- these have no `name` field, so `nameOf` reports them as "<anonymous>"
|
|
46
|
+
// rather than skipping them outright.
|
|
47
|
+
function_expression: "function",
|
|
48
|
+
class_declaration: "class",
|
|
49
|
+
class: "class",
|
|
50
|
+
method_definition: "method",
|
|
51
|
+
interface_declaration: "interface",
|
|
52
|
+
type_alias_declaration: "type",
|
|
53
|
+
});
|
|
54
|
+
function extensionOf(path) {
|
|
55
|
+
const dot = path.lastIndexOf(".");
|
|
56
|
+
return dot === -1 ? "" : path.slice(dot);
|
|
57
|
+
}
|
|
58
|
+
/** Pure: map a file path to the grammar name that would parse it, or null if unsupported. */
|
|
59
|
+
export function resolveRepoMapLanguage(path) {
|
|
60
|
+
return LANGUAGE_BY_EXTENSION[extensionOf(path)] ?? null;
|
|
61
|
+
}
|
|
62
|
+
let parserInitialized = null;
|
|
63
|
+
async function defaultLoadRepoMapLanguage(languageName) {
|
|
64
|
+
parserInitialized ??= Parser.init();
|
|
65
|
+
await parserInitialized;
|
|
66
|
+
const wasmPath = requireFromHere().resolve(`tree-sitter-wasms/out/tree-sitter-${languageName}.wasm`);
|
|
67
|
+
return Parser.Language.load(readFileSync(wasmPath));
|
|
68
|
+
}
|
|
69
|
+
const DEFAULT_MAX_FILES = 200;
|
|
70
|
+
const DEFAULT_MAX_SOURCE_BYTES = 1_000_000;
|
|
71
|
+
const DEFAULT_MAX_TOTAL_SOURCE_BYTES = 5_000_000;
|
|
72
|
+
const DEFAULT_MAX_AST_NODES = 50_000;
|
|
73
|
+
const DEFAULT_MAX_SYMBOLS = 5_000;
|
|
74
|
+
const MAX_NAME_CHARS = 200;
|
|
75
|
+
function boundedNodeText(sourceText, node, maxChars) {
|
|
76
|
+
return sourceText.slice(node.startIndex, Math.min(node.endIndex, node.startIndex + maxChars));
|
|
77
|
+
}
|
|
78
|
+
/** First line of a symbol node's own text, trimmed and bounded to `maxChars` (with an ellipsis marker when cut),
|
|
79
|
+
* so one huge one-line minified function can't blow out the rendered output on its own. */
|
|
80
|
+
function signatureOf(sourceText, node, maxChars) {
|
|
81
|
+
const end = Math.min(node.endIndex, node.startIndex + maxChars + 1);
|
|
82
|
+
const newline = sourceText.indexOf("\n", node.startIndex);
|
|
83
|
+
const sliceEnd = newline === -1 || newline > end ? end : newline;
|
|
84
|
+
const firstLine = sourceText.slice(node.startIndex, sliceEnd).trim();
|
|
85
|
+
return node.endIndex - node.startIndex > maxChars &&
|
|
86
|
+
firstLine.length >= maxChars
|
|
87
|
+
? `${firstLine.slice(0, maxChars)}…`
|
|
88
|
+
: firstLine;
|
|
89
|
+
}
|
|
90
|
+
function nameOf(sourceText, node) {
|
|
91
|
+
const nameNode = node.childForFieldName("name");
|
|
92
|
+
if (!nameNode)
|
|
93
|
+
return "<anonymous>";
|
|
94
|
+
const name = boundedNodeText(sourceText, nameNode, MAX_NAME_CHARS + 1);
|
|
95
|
+
return name.length > MAX_NAME_CHARS
|
|
96
|
+
? `${name.slice(0, MAX_NAME_CHARS)}…`
|
|
97
|
+
: name;
|
|
98
|
+
}
|
|
99
|
+
export function extractRepoMapSymbols(tree, sourceTextOrMaxSignatureChars = tree.rootNode.text, options = {}) {
|
|
100
|
+
const sourceText = typeof sourceTextOrMaxSignatureChars === "string"
|
|
101
|
+
? sourceTextOrMaxSignatureChars
|
|
102
|
+
: tree.rootNode.text;
|
|
103
|
+
const maxSignatureChars = typeof sourceTextOrMaxSignatureChars === "number"
|
|
104
|
+
? sourceTextOrMaxSignatureChars
|
|
105
|
+
: (options.maxSignatureChars ?? 120);
|
|
106
|
+
const maxAstNodes = options.maxAstNodes ?? DEFAULT_MAX_AST_NODES;
|
|
107
|
+
const maxSymbols = options.maxSymbols ?? DEFAULT_MAX_SYMBOLS;
|
|
108
|
+
const symbols = [];
|
|
109
|
+
const stack = [tree.rootNode];
|
|
110
|
+
let visited = 0;
|
|
111
|
+
while (stack.length > 0) {
|
|
112
|
+
const node = stack.pop();
|
|
113
|
+
visited += 1;
|
|
114
|
+
if (visited > maxAstNodes)
|
|
115
|
+
return null;
|
|
116
|
+
const kind = SYMBOL_NODE_KIND[node.type];
|
|
117
|
+
if (kind) {
|
|
118
|
+
if (symbols.length >= maxSymbols)
|
|
119
|
+
return null;
|
|
120
|
+
symbols.push({
|
|
121
|
+
kind,
|
|
122
|
+
name: nameOf(sourceText, node),
|
|
123
|
+
signature: signatureOf(sourceText, node, maxSignatureChars),
|
|
124
|
+
line: node.startPosition.row + 1,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
for (let index = node.namedChildCount - 1; index >= 0; index -= 1) {
|
|
128
|
+
stack.push(node.namedChild(index));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return symbols;
|
|
132
|
+
}
|
|
133
|
+
/** Build one `RepoMapFileEntry` per source file: unsupported extensions and grammar/parse failures are caught
|
|
134
|
+
* and reported via `skipped`, never thrown -- see module header. A language's grammar is only loaded once per
|
|
135
|
+
* call even across many files of the same language. */
|
|
136
|
+
export async function buildRepoMap(files, options = {}) {
|
|
137
|
+
const loadLanguage = options.loadLanguage ?? defaultLoadRepoMapLanguage;
|
|
138
|
+
const maxSignatureChars = options.maxSignatureChars ?? 120;
|
|
139
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
140
|
+
const maxSourceBytes = options.maxSourceBytes ?? DEFAULT_MAX_SOURCE_BYTES;
|
|
141
|
+
const maxTotalSourceBytes = options.maxTotalSourceBytes ?? DEFAULT_MAX_TOTAL_SOURCE_BYTES;
|
|
142
|
+
const maxAstNodes = options.maxAstNodes ?? DEFAULT_MAX_AST_NODES;
|
|
143
|
+
const maxSymbols = options.maxSymbols ?? DEFAULT_MAX_SYMBOLS;
|
|
144
|
+
const languageCache = new Map();
|
|
145
|
+
async function resolveLanguage(name) {
|
|
146
|
+
const cached = languageCache.get(name);
|
|
147
|
+
if (cached !== undefined)
|
|
148
|
+
return cached;
|
|
149
|
+
try {
|
|
150
|
+
const language = await loadLanguage(name);
|
|
151
|
+
languageCache.set(name, language);
|
|
152
|
+
return language;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
languageCache.set(name, null);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const entries = [];
|
|
160
|
+
let totalSourceBytes = 0;
|
|
161
|
+
for (const [index, file] of files.entries()) {
|
|
162
|
+
if (index >= maxFiles) {
|
|
163
|
+
entries.push({
|
|
164
|
+
path: file.path,
|
|
165
|
+
language: resolveRepoMapLanguage(file.path),
|
|
166
|
+
symbols: [],
|
|
167
|
+
skipped: "resource_limit",
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const languageName = resolveRepoMapLanguage(file.path);
|
|
172
|
+
const sourceBytes = Buffer.byteLength(file.sourceText, "utf8");
|
|
173
|
+
totalSourceBytes += sourceBytes;
|
|
174
|
+
if (sourceBytes > maxSourceBytes ||
|
|
175
|
+
totalSourceBytes > maxTotalSourceBytes) {
|
|
176
|
+
entries.push({
|
|
177
|
+
path: file.path,
|
|
178
|
+
language: languageName,
|
|
179
|
+
symbols: [],
|
|
180
|
+
skipped: "resource_limit",
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!languageName) {
|
|
185
|
+
entries.push({
|
|
186
|
+
path: file.path,
|
|
187
|
+
language: null,
|
|
188
|
+
symbols: [],
|
|
189
|
+
skipped: "unsupported_language",
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const language = await resolveLanguage(languageName);
|
|
194
|
+
if (!language) {
|
|
195
|
+
entries.push({
|
|
196
|
+
path: file.path,
|
|
197
|
+
language: languageName,
|
|
198
|
+
symbols: [],
|
|
199
|
+
skipped: "grammar_unavailable",
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const parser = new Parser();
|
|
205
|
+
parser.setLanguage(language);
|
|
206
|
+
const tree = parser.parse(file.sourceText);
|
|
207
|
+
const symbols = extractRepoMapSymbols(tree, file.sourceText, {
|
|
208
|
+
maxSignatureChars,
|
|
209
|
+
maxAstNodes,
|
|
210
|
+
maxSymbols,
|
|
211
|
+
});
|
|
212
|
+
entries.push({
|
|
213
|
+
path: file.path,
|
|
214
|
+
language: languageName,
|
|
215
|
+
symbols: symbols ?? [],
|
|
216
|
+
...(symbols === null ? { skipped: "resource_limit" } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
entries.push({
|
|
221
|
+
path: file.path,
|
|
222
|
+
language: languageName,
|
|
223
|
+
symbols: [],
|
|
224
|
+
skipped: "grammar_unavailable",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return entries;
|
|
229
|
+
}
|
|
230
|
+
/** Render entries into a bounded plain-text outline: one line per symbol (`kind name (line N): signature`),
|
|
231
|
+
* skipped/empty files noted with a one-line placeholder. Stops once `maxOutputChars` would be exceeded and
|
|
232
|
+
* appends a truncation marker, so a caller/prompt-builder can tell the map is partial rather than complete. */
|
|
233
|
+
export function renderRepoMap(entries, maxOutputChars = 20_000) {
|
|
234
|
+
const lines = [];
|
|
235
|
+
let length = 0;
|
|
236
|
+
let truncated = false;
|
|
237
|
+
function pushLine(line) {
|
|
238
|
+
const addedLength = length === 0 ? line.length : line.length + 1; // +1 for the joining newline
|
|
239
|
+
if (length + addedLength > maxOutputChars) {
|
|
240
|
+
truncated = true;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
lines.push(line);
|
|
244
|
+
length += addedLength;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
outer: for (const entry of entries) {
|
|
248
|
+
if (entry.skipped) {
|
|
249
|
+
if (!pushLine(`${entry.path}: (skipped: ${entry.skipped})`))
|
|
250
|
+
break outer;
|
|
251
|
+
}
|
|
252
|
+
else if (entry.symbols.length === 0) {
|
|
253
|
+
if (!pushLine(`${entry.path}: (no symbols)`))
|
|
254
|
+
break outer;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
if (!pushLine(`${entry.path}:`))
|
|
258
|
+
break outer;
|
|
259
|
+
for (const symbol of entry.symbols) {
|
|
260
|
+
if (!pushLine(` ${symbol.kind} ${symbol.name} (line ${symbol.line}): ${symbol.signature}`))
|
|
261
|
+
break outer;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (truncated)
|
|
266
|
+
lines.push("… (repo map truncated to fit the output budget)");
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|