@bridge_gpt/mcp-server 0.2.53 → 0.2.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -56
- package/build/agent-guidance.generated.js +3 -0
- package/build/agent-launchers/claude-executor-adapter.js +3 -0
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/agent-notes.js +178 -0
- package/build/agent-registry.js +5 -2
- package/build/agent-utils.js +58 -0
- package/build/agents.generated.js +1 -1
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/codex-skill-adapter.js +55 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +235 -14
- package/build/conductor/bring-up-facts.js +187 -0
- package/build/conductor/claude-hook.js +7 -5
- package/build/conductor/cli.js +28 -0
- package/build/conductor/doctor.js +80 -9
- package/build/conductor/epic-implementer-cli.js +1298 -0
- package/build/conductor/epic-runtime.js +134 -98
- package/build/conductor/errors.js +2 -2
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-hooks.js +28 -14
- package/build/conductor/install-doctor.js +11 -5
- package/build/conductor/readiness-cli.js +10 -10
- package/build/conductor/readiness-sections.js +58 -9
- package/build/conductor/readiness.js +120 -4
- package/build/conductor/recovery-cli.js +289 -10
- package/build/conductor/recovery-operations.js +125 -2
- package/build/conductor/repair-contract.js +58 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/store.js +2 -2
- package/build/conductor/supervisor-runtime.js +1 -1
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +2 -139
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
- package/build/conductor-removed-stub-bin.js +30 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +145 -18
- package/build/drive-epic.js +752 -90
- package/build/epic-implementer-bin.js +145 -0
- package/build/epic-implementer-bundle-cli.js +264 -0
- package/build/epic-implementer-claude-hook-bin.js +3 -0
- package/build/epic-integration-pr.js +5 -3
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/env.js +6 -0
- package/build/executor/interrupted-worktree.js +60 -0
- package/build/executor/job-errors.js +45 -0
- package/build/executor/job-runner.js +334 -9
- package/build/executor/job-types.js +25 -9
- package/build/executor/merge-tree-classifier.js +171 -0
- package/build/executor/reconcile-overlap-governance.js +129 -0
- package/build/executor/reconcile-overlap-job.js +989 -0
- package/build/executor/reconcile-overlap-types.js +14 -0
- package/build/executor/spawn-job-driver.js +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/worker-finalization.js +25 -2
- package/build/executor/worker-guard-hook.js +15 -7
- package/build/implement-epic/bridge-client.js +773 -0
- package/build/implement-epic/checkpoint-store.js +542 -0
- package/build/implement-epic/cli.js +3158 -0
- package/build/implement-epic/cut-protocol.js +392 -0
- package/build/implement-epic/lock.js +302 -0
- package/build/implement-epic/pr-state.js +286 -0
- package/build/implement-epic/spawn.js +113 -0
- package/build/index.js +657 -481
- package/build/init.js +72 -8
- package/build/install-bridge-conductor.js +5 -5
- package/build/install-bridge.js +403 -70
- package/build/local-artifact-storage.js +130 -0
- package/build/mcp-host-config.js +22 -60
- package/build/mcp-host-entry-adapter.js +18 -0
- package/build/mcp-host-targets.js +1 -21
- package/build/merge-pull-request.js +1 -1
- package/build/pipelines.generated.js +23 -16
- package/build/plan-epic-conductor-eligibility.js +1 -1
- package/build/plane/cli.js +321 -41
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/preflight.js +128 -12
- package/build/plane/shutdown.js +18 -5
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readiness-check.js +3 -3
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +6 -1
- package/build/setup-epic.js +567 -38
- package/build/start-tickets-conductor.js +8 -7
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +1 -1
- package/docs/CONDUCTOR.md +8 -6
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +5 -3
- package/pipelines/review-ticket.json +17 -4
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic merge classification for `reconcile_overlap` (BAPI-1152).
|
|
3
|
+
*
|
|
4
|
+
* Classifies a (child head, required epic SHA) pair with
|
|
5
|
+
* `git merge-tree --write-tree` BEFORE anything touches the held worktree.
|
|
6
|
+
* `merge-tree` computes the merge entirely in the object database: it writes a
|
|
7
|
+
* tree object and reports conflicts, but never touches a working tree, an index,
|
|
8
|
+
* `HEAD`, or a ref. That is what lets the job decide "clean or conflicted" first
|
|
9
|
+
* and only then materialize a real merge.
|
|
10
|
+
*
|
|
11
|
+
* Output is read in the machine-readable `-z --name-only --messages` form, never
|
|
12
|
+
* scraped from human text. Verified against git 2.50.1:
|
|
13
|
+
*
|
|
14
|
+
* <tree-oid> NUL <conflicted path> NUL ... NUL NUL
|
|
15
|
+
* ( <N> NUL <path 1> NUL ... <path N> NUL <type> NUL <message> NUL )*
|
|
16
|
+
*
|
|
17
|
+
* with exit 0 for a clean merge and 1 for a conflicted one. Exit 1 is ALSO what
|
|
18
|
+
* git returns for an unknown commit — with no tree OID on stdout — so the exit
|
|
19
|
+
* code alone never decides "conflicted": the parsed tree OID does. Anything that
|
|
20
|
+
* is not a proven clean or conflicted result is `operational_error`, and the job
|
|
21
|
+
* fails rather than guessing.
|
|
22
|
+
*
|
|
23
|
+
* Every git call is an argv list through the injected `RunCommand` (`execFile`,
|
|
24
|
+
* `shell: false`); no shell string is ever built.
|
|
25
|
+
*/
|
|
26
|
+
import { normalizeRepoRelativePath } from "../conductor/file-scope-guard.js";
|
|
27
|
+
/** Bound on the conflicted paths recorded. More than this is still `conflicted`. */
|
|
28
|
+
export const MERGE_TREE_MAX_CONFLICTS = 200;
|
|
29
|
+
/** Bound on the informational lines kept per conflicted path. */
|
|
30
|
+
export const MERGE_TREE_MAX_MESSAGES_PER_PATH = 5;
|
|
31
|
+
/** Bound on one message line. */
|
|
32
|
+
export const MERGE_TREE_MESSAGE_MAX_CHARS = 300;
|
|
33
|
+
/** Wall-clock bound on each git call the classifier makes. */
|
|
34
|
+
export const MERGE_TREE_COMMAND_TIMEOUT_MS = 120_000;
|
|
35
|
+
const OID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
36
|
+
function descriptorFor(type, message) {
|
|
37
|
+
// The MESSAGE carries git's user-facing spelling ("CONFLICT (content)"); the
|
|
38
|
+
// type token is an internal name ("CONFLICT (contents)"). Prefer the message's
|
|
39
|
+
// own prefix so the descriptor matches what an operator sees in `git merge`.
|
|
40
|
+
const fromMessage = /^(CONFLICT \([^)]*\))/.exec(message.trim());
|
|
41
|
+
if (fromMessage)
|
|
42
|
+
return fromMessage[1];
|
|
43
|
+
return type.trim().length > 0 ? type.trim() : "CONFLICT (unknown)";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse `git merge-tree --write-tree -z --name-only --messages` stdout.
|
|
47
|
+
*
|
|
48
|
+
* Exported for the unit tests; `classifyMergeTree` is the runtime entry point.
|
|
49
|
+
* Returns `null` when stdout does not start with a tree OID — the caller treats
|
|
50
|
+
* that as an operational error.
|
|
51
|
+
*/
|
|
52
|
+
export function parseMergeTreeOutput(stdout) {
|
|
53
|
+
const tokens = stdout.split("\0");
|
|
54
|
+
const treeOid = (tokens[0] ?? "").trim().toLowerCase();
|
|
55
|
+
if (!OID_RE.test(treeOid))
|
|
56
|
+
return null;
|
|
57
|
+
let i = 1;
|
|
58
|
+
const conflictedPaths = [];
|
|
59
|
+
while (i < tokens.length && tokens[i] !== "") {
|
|
60
|
+
conflictedPaths.push(tokens[i]);
|
|
61
|
+
i += 1;
|
|
62
|
+
}
|
|
63
|
+
i += 1; // the empty token separating conflicted paths from messages
|
|
64
|
+
const byPath = new Map();
|
|
65
|
+
const record = (rawPath, descriptor, message) => {
|
|
66
|
+
const path = normalizeRepoRelativePath(rawPath) ?? rawPath;
|
|
67
|
+
let entry = byPath.get(path);
|
|
68
|
+
if (!entry) {
|
|
69
|
+
entry = { path, descriptor: descriptor ?? "CONFLICT (unknown)", messages: [] };
|
|
70
|
+
byPath.set(path, entry);
|
|
71
|
+
}
|
|
72
|
+
else if (descriptor && entry.descriptor === "CONFLICT (unknown)") {
|
|
73
|
+
entry.descriptor = descriptor;
|
|
74
|
+
}
|
|
75
|
+
if (message && entry.messages.length < MERGE_TREE_MAX_MESSAGES_PER_PATH) {
|
|
76
|
+
entry.messages.push(message.trim().slice(0, MERGE_TREE_MESSAGE_MAX_CHARS));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
while (i < tokens.length) {
|
|
80
|
+
const countToken = tokens[i];
|
|
81
|
+
if (countToken === "" || countToken === undefined)
|
|
82
|
+
break;
|
|
83
|
+
const count = Number.parseInt(countToken, 10);
|
|
84
|
+
// A group is `count` paths plus a type and a message; a truncated group ends
|
|
85
|
+
// the parse rather than reading past stdout.
|
|
86
|
+
if (!Number.isInteger(count) || count < 0 || i + count + 2 >= tokens.length)
|
|
87
|
+
break;
|
|
88
|
+
const paths = tokens.slice(i + 1, i + 1 + count);
|
|
89
|
+
const type = tokens[i + 1 + count] ?? "";
|
|
90
|
+
const message = tokens[i + 2 + count] ?? "";
|
|
91
|
+
i += count + 3;
|
|
92
|
+
if (!type.startsWith("CONFLICT"))
|
|
93
|
+
continue;
|
|
94
|
+
const descriptor = descriptorFor(type, message);
|
|
95
|
+
// The FIRST path of a multi-path message (e.g. rename/delete names the new
|
|
96
|
+
// and the old path) is the one git reports as conflicted; the rest are
|
|
97
|
+
// context. Recording every named path would report a path that no longer
|
|
98
|
+
// exists in the merge result as conflicted.
|
|
99
|
+
const conflicted = paths.find((p) => conflictedPaths.includes(p)) ?? paths[0];
|
|
100
|
+
if (conflicted !== undefined)
|
|
101
|
+
record(conflicted, descriptor, message);
|
|
102
|
+
}
|
|
103
|
+
// A path git listed as conflicted with no CONFLICT message is still conflicted.
|
|
104
|
+
for (const path of conflictedPaths) {
|
|
105
|
+
const normalized = normalizeRepoRelativePath(path) ?? path;
|
|
106
|
+
if (!byPath.has(normalized))
|
|
107
|
+
record(path, null, null);
|
|
108
|
+
}
|
|
109
|
+
const conflicts = Array.from(byPath.values())
|
|
110
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
111
|
+
.slice(0, MERGE_TREE_MAX_CONFLICTS);
|
|
112
|
+
return { treeOid, conflicts };
|
|
113
|
+
}
|
|
114
|
+
/** Verify a commit object is present locally. */
|
|
115
|
+
async function commitPresent(runCommand, repoDir, sha) {
|
|
116
|
+
try {
|
|
117
|
+
const result = await runCommand("git", ["cat-file", "-e", `${sha}^{commit}`], {
|
|
118
|
+
cwd: repoDir,
|
|
119
|
+
timeoutMs: MERGE_TREE_COMMAND_TIMEOUT_MS,
|
|
120
|
+
});
|
|
121
|
+
return result.exitCode === 0;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Classify merging `requiredEpicSha` into `childHead`. Never mutates a worktree,
|
|
129
|
+
* an index, `HEAD`, or a ref. Both SHAs must already be present in `repoDir`'s
|
|
130
|
+
* object database (the caller fetches them).
|
|
131
|
+
*/
|
|
132
|
+
export async function classifyMergeTree(runCommand, repoDir, childHead, requiredEpicSha) {
|
|
133
|
+
for (const sha of [childHead, requiredEpicSha]) {
|
|
134
|
+
if (!OID_RE.test(sha)) {
|
|
135
|
+
return { kind: "operational_error", reason: "a merge input is not a full commit SHA" };
|
|
136
|
+
}
|
|
137
|
+
if (!(await commitPresent(runCommand, repoDir, sha))) {
|
|
138
|
+
return {
|
|
139
|
+
kind: "operational_error",
|
|
140
|
+
reason: `commit ${sha.slice(0, 12)} is not present in the local object database`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
result = await runCommand("git", ["merge-tree", "--write-tree", "-z", "--name-only", "--messages", childHead, requiredEpicSha], { cwd: repoDir, timeoutMs: MERGE_TREE_COMMAND_TIMEOUT_MS });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return { kind: "operational_error", reason: "git merge-tree could not be run" };
|
|
150
|
+
}
|
|
151
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
152
|
+
return { kind: "operational_error", reason: `git merge-tree exited ${result.exitCode}` };
|
|
153
|
+
}
|
|
154
|
+
const parsed = parseMergeTreeOutput(result.stdout);
|
|
155
|
+
if (parsed === null) {
|
|
156
|
+
return { kind: "operational_error", reason: "git merge-tree produced no tree object" };
|
|
157
|
+
}
|
|
158
|
+
if (result.exitCode === 0) {
|
|
159
|
+
return { kind: "clean", treeOid: parsed.treeOid };
|
|
160
|
+
}
|
|
161
|
+
if (parsed.conflicts.length === 0) {
|
|
162
|
+
// Exit 1 WITH a tree but no conflict record: git said "not clean" and did not
|
|
163
|
+
// say why. Unknown is conflicted, never clean.
|
|
164
|
+
return {
|
|
165
|
+
kind: "conflicted",
|
|
166
|
+
treeOid: parsed.treeOid,
|
|
167
|
+
conflicts: [{ path: "(unreported)", descriptor: "CONFLICT (unknown)", messages: [] }],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { kind: "conflicted", treeOid: parsed.treeOid, conflicts: parsed.conflicts };
|
|
171
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governance-region comparison for `reconcile_overlap` (BAPI-1152).
|
|
3
|
+
*
|
|
4
|
+
* Before the wrapper pushes, the candidate tree is compared against the
|
|
5
|
+
* governance-guarded regions the SERVER found in the INPUT epic head
|
|
6
|
+
* (`required_epic_sha`). A candidate that changes any protected region — its
|
|
7
|
+
* content OR either marker line — is never pushed: the job reports the
|
|
8
|
+
* `governance_guard_change_required` fact and the reconciler parks naming the tag.
|
|
9
|
+
*
|
|
10
|
+
* This module deliberately does NOT parse the marker grammar. The server owns it
|
|
11
|
+
* (`api/library/epic_conductor/governance_markers.py`) and ships each region as
|
|
12
|
+
* its exact BEGIN and END marker lines plus the SHA-256 of the region's content,
|
|
13
|
+
* markers included. Here a region is located by those exact lines and compared by
|
|
14
|
+
* hash — never by line number, which an edit above the region would shift without
|
|
15
|
+
* touching anything it protects. A second grammar implementation would be a
|
|
16
|
+
* second place for the two to disagree about what is protected.
|
|
17
|
+
*
|
|
18
|
+
* Fail-closed everywhere the comparison cannot be proven:
|
|
19
|
+
* - the region's file is gone from the candidate (deleted or renamed), binary,
|
|
20
|
+
* or unreadable;
|
|
21
|
+
* - a marker line is missing, duplicated, or out of order;
|
|
22
|
+
* - the server reported the file's marker structure unprovable (unbalanced,
|
|
23
|
+
* nested, mismatched, or over the scan bound) and the candidate changes it;
|
|
24
|
+
* - the candidate changes a file the server did NOT scan and either side of that
|
|
25
|
+
* change contains a marker token — the server made no statement about it.
|
|
26
|
+
* The last two carry the synthetic tag {@link UNPROVABLE_GOVERNANCE_TAG}, which is
|
|
27
|
+
* itself valid marker grammar, so the park still names something greppable.
|
|
28
|
+
*/
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
/** The synthetic tag for protection the comparison could not prove. */
|
|
31
|
+
export const UNPROVABLE_GOVERNANCE_TAG = "GOVERNANCE_MARKERS_UNPROVABLE";
|
|
32
|
+
/** The token every governance marker line contains. */
|
|
33
|
+
export const GOVERNANCE_MARKER_TOKEN = "BRIDGE-GOVERNANCE-GUARD-";
|
|
34
|
+
/** Bounds on the reported fact (mirrors the server's result-model bounds). */
|
|
35
|
+
export const GOVERNANCE_FACT_MAX_FINDINGS = 8;
|
|
36
|
+
export const GOVERNANCE_FACT_MAX_PATHS = 8;
|
|
37
|
+
/**
|
|
38
|
+
* Split text into lines exactly the way Python's `str.splitlines()` does, so the
|
|
39
|
+
* region hash computed here matches the one the server computed.
|
|
40
|
+
*/
|
|
41
|
+
export function splitLinesLikePython(text) {
|
|
42
|
+
const lines = text.split(/\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/);
|
|
43
|
+
// `splitlines()` yields no trailing empty entry for a terminal line break.
|
|
44
|
+
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
45
|
+
lines.pop();
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
function sha256(text) {
|
|
49
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
50
|
+
}
|
|
51
|
+
function indicesOf(lines, needle) {
|
|
52
|
+
const out = [];
|
|
53
|
+
lines.forEach((line, index) => {
|
|
54
|
+
if (line === needle)
|
|
55
|
+
out.push(index);
|
|
56
|
+
});
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** True when a region is intact in `text`: both markers once, in order, same hash. */
|
|
60
|
+
function regionIntact(text, region) {
|
|
61
|
+
const lines = splitLinesLikePython(text);
|
|
62
|
+
const begins = indicesOf(lines, region.beginMarker);
|
|
63
|
+
const ends = indicesOf(lines, region.endMarker);
|
|
64
|
+
if (begins.length !== 1 || ends.length !== 1)
|
|
65
|
+
return false;
|
|
66
|
+
const [begin] = begins;
|
|
67
|
+
const [end] = ends;
|
|
68
|
+
if (end < begin)
|
|
69
|
+
return false;
|
|
70
|
+
return sha256(lines.slice(begin, end + 1).join("\n")) === region.contentSha256;
|
|
71
|
+
}
|
|
72
|
+
function containsMarkerToken(read) {
|
|
73
|
+
// An unreadable or binary side cannot be proven marker-free.
|
|
74
|
+
if (read.kind === "unreadable" || read.kind === "binary")
|
|
75
|
+
return true;
|
|
76
|
+
return read.kind === "text" && read.text.includes(GOVERNANCE_MARKER_TOKEN);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compare the candidate against the input epic head's governance regions.
|
|
80
|
+
* Returns the structured fact, or `null` when nothing protected is touched.
|
|
81
|
+
*/
|
|
82
|
+
export async function scanGovernanceRegions(input) {
|
|
83
|
+
const hits = new Map();
|
|
84
|
+
const hit = (tag, path) => {
|
|
85
|
+
let paths = hits.get(tag);
|
|
86
|
+
if (!paths) {
|
|
87
|
+
paths = new Set();
|
|
88
|
+
hits.set(tag, paths);
|
|
89
|
+
}
|
|
90
|
+
paths.add(path);
|
|
91
|
+
};
|
|
92
|
+
const changed = new Set(input.changedPaths);
|
|
93
|
+
for (const region of input.snapshot.regions) {
|
|
94
|
+
const candidate = await input.readCandidate(region.path);
|
|
95
|
+
if (candidate.kind !== "text" || !regionIntact(candidate.text, region)) {
|
|
96
|
+
hit(region.tag, region.path);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const finding of input.snapshot.findings) {
|
|
100
|
+
if (changed.has(finding.path))
|
|
101
|
+
hit(UNPROVABLE_GOVERNANCE_TAG, finding.path);
|
|
102
|
+
}
|
|
103
|
+
const scanned = new Set(input.snapshot.scannedPaths);
|
|
104
|
+
for (const path of input.changedPaths) {
|
|
105
|
+
if (scanned.has(path))
|
|
106
|
+
continue;
|
|
107
|
+
const [candidate, original] = await Promise.all([
|
|
108
|
+
input.readCandidate(path),
|
|
109
|
+
input.readInput(path),
|
|
110
|
+
]);
|
|
111
|
+
if (containsMarkerToken(candidate) || containsMarkerToken(original)) {
|
|
112
|
+
hit(UNPROVABLE_GOVERNANCE_TAG, path);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (hits.size === 0)
|
|
116
|
+
return null;
|
|
117
|
+
const findings = Array.from(hits.entries())
|
|
118
|
+
.map(([tag, paths]) => ({ tag, paths: Array.from(paths).sort().slice(0, GOVERNANCE_FACT_MAX_PATHS) }))
|
|
119
|
+
.sort((a, b) => a.tag !== b.tag ? (a.tag < b.tag ? -1 : 1) : (a.paths[0] ?? "") < (b.paths[0] ?? "") ? -1 : 1);
|
|
120
|
+
const bounded = findings.slice(0, GOVERNANCE_FACT_MAX_FINDINGS);
|
|
121
|
+
const primary = bounded[0];
|
|
122
|
+
return {
|
|
123
|
+
tag: primary.tag,
|
|
124
|
+
paths: primary.paths,
|
|
125
|
+
findings: bounded,
|
|
126
|
+
truncated: findings.length > GOVERNANCE_FACT_MAX_FINDINGS ||
|
|
127
|
+
Array.from(hits.values()).some((paths) => paths.size > GOVERNANCE_FACT_MAX_PATHS),
|
|
128
|
+
};
|
|
129
|
+
}
|