thumbgate 1.14.1 → 1.16.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/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +5 -5
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +60 -35
- package/adapters/chatgpt/openapi.yaml +118 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +217 -84
- package/adapters/opencode/opencode.json +1 -1
- package/bench/prompt-eval-suite.json +5 -1
- package/bin/cli.js +211 -8
- package/config/enforcement.json +59 -7
- package/config/evals/agent-safety-eval.json +338 -22
- package/config/gates/default.json +33 -0
- package/config/gates/routine.json +43 -0
- package/config/github-about.json +3 -3
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +2 -1
- package/config/model-candidates.json +131 -0
- package/openapi/openapi.yaml +118 -2
- package/package.json +70 -51
- package/public/blog.html +7 -7
- package/public/codex-plugin.html +13 -7
- package/public/compare.html +29 -23
- package/public/dashboard.html +105 -12
- package/public/guide.html +28 -28
- package/public/index.html +233 -97
- package/public/learn.html +87 -20
- package/public/lessons.html +26 -2
- package/public/numbers.html +271 -0
- package/public/pro.html +89 -19
- package/scripts/agent-audit-trace.js +55 -0
- package/scripts/agent-memory-lifecycle.js +96 -0
- package/scripts/agent-readiness-plan.js +118 -0
- package/scripts/agentic-data-pipeline.js +21 -1
- package/scripts/agents-sdk-sandbox-plan.js +57 -0
- package/scripts/ai-org-governance.js +98 -0
- package/scripts/ai-search-distribution.js +43 -0
- package/scripts/artifact-agent-plan.js +81 -0
- package/scripts/billing.js +27 -8
- package/scripts/cli-feedback.js +2 -1
- package/scripts/cli-schema.js +60 -5
- package/scripts/code-mode-mcp-plan.js +71 -0
- package/scripts/commercial-offer.js +1 -1
- package/scripts/context-engine.js +1 -2
- package/scripts/context-manager.js +4 -1
- package/scripts/contextfs.js +214 -32
- package/scripts/dashboard-render-spec.js +1 -1
- package/scripts/dashboard.js +275 -9
- package/scripts/decision-journal.js +13 -3
- package/scripts/document-workflow-governance.js +62 -0
- package/scripts/enterprise-agent-rollout.js +34 -0
- package/scripts/experience-replay-governance.js +69 -0
- package/scripts/export-hf-dataset.js +1 -1
- package/scripts/feedback-loop.js +141 -9
- package/scripts/feedback-to-rules.js +17 -23
- package/scripts/gates-engine.js +4 -6
- package/scripts/growth-campaigns.js +49 -0
- package/scripts/harness-selector.js +145 -1
- package/scripts/hybrid-supervisor-agent.js +64 -0
- package/scripts/inference-cache-policy.js +72 -0
- package/scripts/inference-economics.js +53 -0
- package/scripts/internal-agent-bootstrap.js +12 -2
- package/scripts/knowledge-layer-plan.js +108 -0
- package/scripts/lesson-canonical.js +181 -0
- package/scripts/lesson-db.js +71 -10
- package/scripts/lesson-inference.js +183 -44
- package/scripts/lesson-search.js +4 -1
- package/scripts/lesson-synthesis.js +23 -2
- package/scripts/llm-client.js +157 -26
- package/scripts/mailer/resend-mailer.js +112 -1
- package/scripts/mcp-transport-strategy.js +66 -0
- package/scripts/memory-store-governance.js +60 -0
- package/scripts/meta-agent-loop.js +7 -13
- package/scripts/model-access-eligibility.js +38 -0
- package/scripts/model-migration-readiness.js +55 -0
- package/scripts/native-messaging-audit.js +514 -0
- package/scripts/operational-integrity.js +96 -3
- package/scripts/otel-declarative-config.js +56 -0
- package/scripts/perplexity-client.js +1 -1
- package/scripts/post-training-governance.js +34 -0
- package/scripts/pr-manager.js +47 -7
- package/scripts/private-core-boundary.js +72 -0
- package/scripts/production-agent-readiness.js +40 -0
- package/scripts/profile-router.js +16 -1
- package/scripts/prompt-eval.js +564 -32
- package/scripts/prompt-programs.js +93 -0
- package/scripts/provider-action-normalizer.js +585 -0
- package/scripts/rule-validator.js +285 -0
- package/scripts/scaling-law-claims.js +60 -0
- package/scripts/security-scanner.js +1 -1
- package/scripts/self-distill-agent.js +7 -32
- package/scripts/seo-gsd.js +400 -43
- package/scripts/skill-rag-router.js +53 -0
- package/scripts/spec-gate.js +1 -1
- package/scripts/student-consistent-training.js +73 -0
- package/scripts/synthetic-data-provenance.js +98 -0
- package/scripts/task-context-result.js +81 -0
- package/scripts/telemetry-analytics.js +149 -0
- package/scripts/thompson-sampling.js +2 -2
- package/scripts/token-savings.js +7 -6
- package/scripts/token-tco.js +46 -0
- package/scripts/tool-registry.js +75 -3
- package/scripts/verification-loop.js +10 -1
- package/scripts/verifier-scoring.js +71 -0
- package/scripts/workflow-sentinel.js +284 -28
- package/scripts/workspace-agent-routines.js +118 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +434 -120
- package/.claude-plugin/README.md +0 -170
- package/adapters/README.md +0 -12
- package/scripts/analytics-report.js +0 -328
- package/scripts/autonomous-workflow.js +0 -377
- package/scripts/billing-setup.js +0 -109
- package/scripts/creator-campaigns.js +0 -239
- package/scripts/cross-encoder-reranker.js +0 -235
- package/scripts/daemon-manager.js +0 -108
- package/scripts/decision-trace.js +0 -354
- package/scripts/delegation-runtime.js +0 -896
- package/scripts/dispatch-brief.js +0 -159
- package/scripts/distribution-surfaces.js +0 -110
- package/scripts/feedback-history-distiller.js +0 -382
- package/scripts/funnel-analytics.js +0 -35
- package/scripts/history-distiller.js +0 -200
- package/scripts/hosted-job-launcher.js +0 -256
- package/scripts/intent-router.js +0 -392
- package/scripts/lesson-reranker.js +0 -263
- package/scripts/lesson-retrieval.js +0 -148
- package/scripts/managed-lesson-agent.js +0 -183
- package/scripts/operational-dashboard.js +0 -103
- package/scripts/operational-summary.js +0 -129
- package/scripts/operator-artifacts.js +0 -608
- package/scripts/optimize-context.js +0 -17
- package/scripts/org-dashboard.js +0 -206
- package/scripts/partner-orchestration.js +0 -146
- package/scripts/predictive-insights.js +0 -356
- package/scripts/pulse.js +0 -80
- package/scripts/reflector-agent.js +0 -221
- package/scripts/sales-pipeline.js +0 -681
- package/scripts/session-episode-store.js +0 -329
- package/scripts/session-health-sensor.js +0 -242
- package/scripts/session-report.js +0 -120
- package/scripts/swarm-coordinator.js +0 -81
- package/scripts/tool-kpi-tracker.js +0 -12
- package/scripts/webhook-delivery.js +0 -62
- package/scripts/workflow-sprint-intake.js +0 -475
- package/skills/agent-memory/SKILL.md +0 -97
- package/skills/solve-architecture-autonomy/SKILL.md +0 -17
- package/skills/solve-architecture-autonomy/tool.js +0 -33
- package/skills/thumbgate-feedback/SKILL.md +0 -49
|
@@ -110,7 +110,7 @@ function extractCitations(response) {
|
|
|
110
110
|
|
|
111
111
|
class PerplexityClient {
|
|
112
112
|
constructor(options = {}) {
|
|
113
|
-
this.apiKey = options.apiKey
|
|
113
|
+
this.apiKey = options.apiKey ?? process.env.PERPLEXITY_API_KEY ?? '';
|
|
114
114
|
this.baseUrl = options.baseUrl || process.env.PERPLEXITY_BASE_URL || DEFAULT_BASE_URL;
|
|
115
115
|
this.fetchFn = options.fetchFn || globalThis.fetch;
|
|
116
116
|
this.timeoutMs = Number(options.timeoutMs || process.env.PERPLEXITY_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function evaluatePostTrainingPlan(input = {}) {
|
|
5
|
+
const mode = String(input.mode || '').toLowerCase();
|
|
6
|
+
const issues = [];
|
|
7
|
+
if (!['sft', 'rl', 'grpo', 'gspo'].includes(mode)) issues.push('unsupported_post_training_mode');
|
|
8
|
+
if (!input.dataset) issues.push('missing_dataset');
|
|
9
|
+
if (!input.baseCheckpoint) issues.push('missing_base_checkpoint');
|
|
10
|
+
if (input.piiRedacted !== true) issues.push('pii_redaction_required');
|
|
11
|
+
if (input.holdoutEval !== true) issues.push('holdout_eval_required');
|
|
12
|
+
if (input.rewardSpecRequired !== false && ['rl', 'grpo', 'gspo'].includes(mode) && !input.rewardSpec) {
|
|
13
|
+
issues.push('missing_reward_spec');
|
|
14
|
+
}
|
|
15
|
+
if (input.maxSpendCents === undefined) issues.push('missing_spend_cap');
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
mode,
|
|
19
|
+
decision: issues.length === 0 ? 'allow' : 'warn',
|
|
20
|
+
issues,
|
|
21
|
+
requiredArtifacts: [
|
|
22
|
+
'dataset manifest',
|
|
23
|
+
'PII redaction report',
|
|
24
|
+
'base checkpoint',
|
|
25
|
+
'holdout eval report',
|
|
26
|
+
'spend cap',
|
|
27
|
+
['rl', 'grpo', 'gspo'].includes(mode) ? 'reward specification' : null,
|
|
28
|
+
].filter(Boolean),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
evaluatePostTrainingPlan,
|
|
34
|
+
};
|
package/scripts/pr-manager.js
CHANGED
|
@@ -90,9 +90,18 @@ function resolveGhBinary(options = {}) {
|
|
|
90
90
|
throw new Error(`Unable to locate GH CLI in fixed paths: ${candidates.join(', ')}`);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function buildGhEnv(baseEnv = process.env) {
|
|
94
|
+
const env = { ...baseEnv };
|
|
95
|
+
if (!env.GH_TOKEN && !env.GITHUB_TOKEN && env.GH_PAT) {
|
|
96
|
+
env.GH_TOKEN = env.GH_PAT;
|
|
97
|
+
}
|
|
98
|
+
return env;
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
function runGh(args, options = {}) {
|
|
94
102
|
return spawnSync(resolveGhBinary(options), assertSafeGhArgs(args), {
|
|
95
103
|
encoding: 'utf-8',
|
|
104
|
+
env: buildGhEnv(options.env || process.env),
|
|
96
105
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
106
|
});
|
|
98
107
|
}
|
|
@@ -154,7 +163,8 @@ function isOpenPr(pr) {
|
|
|
154
163
|
|
|
155
164
|
function loadManagedPrs(prNumber = '', runner = runGh) {
|
|
156
165
|
if (prNumber) {
|
|
157
|
-
|
|
166
|
+
const explicitPr = getPrStatus(prNumber, runner);
|
|
167
|
+
return isOpenPr(explicitPr) ? [explicitPr] : [];
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
const currentBranchPr = getPrStatus('', runner);
|
|
@@ -336,11 +346,40 @@ function waitForMergeCommit(prNumber, runner = runGh, options = {}) {
|
|
|
336
346
|
};
|
|
337
347
|
}
|
|
338
348
|
|
|
349
|
+
function submitTrunkMergeRequest(prNumber, runner = runGh) {
|
|
350
|
+
const normalizedPrNumber = normalizePrNumber(prNumber, { allowEmpty: false });
|
|
351
|
+
const args = ['pr', 'comment', normalizedPrNumber, '--body', '/trunk merge'];
|
|
352
|
+
console.log(`[PR Manager] Requesting Trunk merge queue for PR #${normalizedPrNumber}...`);
|
|
353
|
+
const result = runner(args);
|
|
354
|
+
if (result.status !== 0) {
|
|
355
|
+
console.error(`[PR Manager] Queue request failed: ${formatGhError(result)}`);
|
|
356
|
+
return { ok: false, mode: 'failed', args, error: formatGhError(result) };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log(`[PR Manager] Queue request accepted for PR #${normalizedPrNumber} (/trunk merge).`);
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
mode: 'queued',
|
|
363
|
+
args,
|
|
364
|
+
finalized: false,
|
|
365
|
+
merged: false,
|
|
366
|
+
reason: 'merge_commit_pending',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
339
370
|
/**
|
|
340
371
|
* Perform autonomous merge
|
|
341
372
|
*/
|
|
342
|
-
function performMerge(
|
|
343
|
-
const
|
|
373
|
+
function performMerge(prInput, runner = runGh, options = {}) {
|
|
374
|
+
const pr = (prInput && typeof prInput === 'object')
|
|
375
|
+
? prInput
|
|
376
|
+
: { number: prInput, baseRefName: options.baseRefName || '' };
|
|
377
|
+
const normalizedPrNumber = normalizePrNumber(pr.number, { allowEmpty: false });
|
|
378
|
+
|
|
379
|
+
if (String(pr.baseRefName || '').toLowerCase() === 'main') {
|
|
380
|
+
return submitTrunkMergeRequest(normalizedPrNumber, runner);
|
|
381
|
+
}
|
|
382
|
+
|
|
344
383
|
const args = ['pr', 'merge', normalizedPrNumber, '--squash', '--delete-branch'];
|
|
345
384
|
console.log(`[PR Manager] Initiating protected squash merge for PR #${normalizedPrNumber}...`);
|
|
346
385
|
const result = runner(args);
|
|
@@ -352,10 +391,10 @@ function performMerge(prNumber, runner = runGh, options = {}) {
|
|
|
352
391
|
? { finalized: false, merged: false, reason: 'merge_commit_pending' }
|
|
353
392
|
: waitForMergeCommit(normalizedPrNumber, runner, options);
|
|
354
393
|
return { ok: true, mode, args, ...mergeStatus };
|
|
355
|
-
} else {
|
|
356
|
-
console.error(`[PR Manager] Merge failed: ${formatGhError(result)}`);
|
|
357
|
-
return { ok: false, mode: 'failed', args, error: formatGhError(result) };
|
|
358
394
|
}
|
|
395
|
+
|
|
396
|
+
console.error(`[PR Manager] Merge failed: ${formatGhError(result)}`);
|
|
397
|
+
return { ok: false, mode: 'failed', args, error: formatGhError(result) };
|
|
359
398
|
}
|
|
360
399
|
|
|
361
400
|
async function managePrs(prNumber = '', runner = runGh, options = {}) {
|
|
@@ -370,7 +409,7 @@ async function managePrs(prNumber = '', runner = runGh, options = {}) {
|
|
|
370
409
|
for (const pr of prs) {
|
|
371
410
|
const outcome = await resolveBlockers(pr, runner);
|
|
372
411
|
if (outcome.status === 'ready') {
|
|
373
|
-
const mergeResult = performMerge(pr
|
|
412
|
+
const mergeResult = performMerge(pr, runner, options);
|
|
374
413
|
outcome.mergeRequested = mergeResult.ok;
|
|
375
414
|
outcome.mergeMode = mergeResult.mode;
|
|
376
415
|
if (mergeResult.mergeCommit) {
|
|
@@ -406,6 +445,7 @@ if (require.main === module) {
|
|
|
406
445
|
|
|
407
446
|
module.exports = {
|
|
408
447
|
assertSafeGhArgs,
|
|
448
|
+
buildGhEnv,
|
|
409
449
|
getPrStatus,
|
|
410
450
|
getPrChecks,
|
|
411
451
|
listOpenPrs,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
function normalizeRequest(request) {
|
|
6
|
+
return String(request || '').trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isOptionalModuleMissing(error, request) {
|
|
10
|
+
if (!error || error.code !== 'MODULE_NOT_FOUND') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const message = String(error?.message || '');
|
|
14
|
+
const normalizedRequest = normalizeRequest(request);
|
|
15
|
+
const basename = path.basename(normalizedRequest);
|
|
16
|
+
return [normalizedRequest, basename]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.some((candidate) => message.includes(candidate));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadOptionalModule(request, fallbackFactory) {
|
|
22
|
+
try {
|
|
23
|
+
return require(request);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!isOptionalModuleMissing(error, request)) {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
return typeof fallbackFactory === 'function'
|
|
29
|
+
? fallbackFactory(error)
|
|
30
|
+
: (fallbackFactory || {});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createUnavailableError(feature, options = {}) {
|
|
35
|
+
const err = new Error(
|
|
36
|
+
`${feature} moved behind the ThumbGate-Core boundary and is unavailable in the public thumbgate package.`
|
|
37
|
+
);
|
|
38
|
+
err.code = 'THUMBGATE_CORE_REQUIRED';
|
|
39
|
+
err.statusCode = options.statusCode || 503;
|
|
40
|
+
err.feature = feature;
|
|
41
|
+
return err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createUnavailableOperation(feature, options = {}) {
|
|
45
|
+
return function unavailableOperation() {
|
|
46
|
+
throw createUnavailableError(feature, options);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createUnavailableAsyncOperation(feature, options = {}) {
|
|
51
|
+
return async function unavailableAsyncOperation() {
|
|
52
|
+
throw createUnavailableError(feature, options);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createUnavailableReport(feature, extra = {}) {
|
|
57
|
+
return {
|
|
58
|
+
available: false,
|
|
59
|
+
source: 'ThumbGate-Core',
|
|
60
|
+
message: `${feature} requires ThumbGate-Core.`,
|
|
61
|
+
...extra,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
createUnavailableAsyncOperation,
|
|
67
|
+
createUnavailableError,
|
|
68
|
+
createUnavailableOperation,
|
|
69
|
+
createUnavailableReport,
|
|
70
|
+
isOptionalModuleMissing,
|
|
71
|
+
loadOptionalModule,
|
|
72
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function readinessStatus(score, missing) {
|
|
5
|
+
if (missing.length === 0) return 'production_ready';
|
|
6
|
+
if (score >= 60) return 'needs_hardening';
|
|
7
|
+
return 'prototype';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function evaluateProductionAgentReadiness(input = {}) {
|
|
11
|
+
const signals = {
|
|
12
|
+
subAgents: Array.isArray(input.subAgents) && input.subAgents.length >= 2,
|
|
13
|
+
structuredOutputs: input.structuredOutputs === true,
|
|
14
|
+
dynamicRag: input.dynamicRag === true,
|
|
15
|
+
observability: input.observability === true || input.tracing === true,
|
|
16
|
+
circuitBreakers: input.circuitBreakers === true,
|
|
17
|
+
};
|
|
18
|
+
const missing = Object.entries(signals)
|
|
19
|
+
.filter(([, present]) => !present)
|
|
20
|
+
.map(([name]) => name);
|
|
21
|
+
const score = Math.round((Object.values(signals).filter(Boolean).length / Object.keys(signals).length) * 100);
|
|
22
|
+
return {
|
|
23
|
+
status: readinessStatus(score, missing),
|
|
24
|
+
score,
|
|
25
|
+
signals,
|
|
26
|
+
missing,
|
|
27
|
+
requiredFixes: missing.map((name) => ({
|
|
28
|
+
subAgents: 'Split monolithic prompts into narrow sub-agent stages.',
|
|
29
|
+
structuredOutputs: 'Use runtime-validated schemas instead of prompt-only JSON formatting.',
|
|
30
|
+
dynamicRag: 'Replace hardcoded context with refreshed retrieval over indexed source material.',
|
|
31
|
+
observability: 'Emit traces for model calls, tool calls, tokens, latency, and stage failures.',
|
|
32
|
+
circuitBreakers: 'Set retry, timeout, loop, and spend limits before production use.',
|
|
33
|
+
}[name])),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
evaluateProductionAgentReadiness,
|
|
39
|
+
readinessStatus,
|
|
40
|
+
};
|
|
@@ -107,6 +107,15 @@ function loadProfilesLazy() {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
const PROFILE_RESTRICTIVENESS_ORDER = Object.freeze([
|
|
111
|
+
'locked',
|
|
112
|
+
'readonly',
|
|
113
|
+
'essential',
|
|
114
|
+
'commerce',
|
|
115
|
+
'dispatch',
|
|
116
|
+
'default',
|
|
117
|
+
]);
|
|
118
|
+
|
|
110
119
|
/**
|
|
111
120
|
* Find the most restrictive (smallest) profile that includes the given tool.
|
|
112
121
|
* Profile restrictiveness = fewer tools allowed = more restricted.
|
|
@@ -114,11 +123,17 @@ function loadProfilesLazy() {
|
|
|
114
123
|
function findMostRestrictiveProfile(toolName) {
|
|
115
124
|
const policy = loadProfilesLazy();
|
|
116
125
|
if (!policy || !policy.profiles) return null;
|
|
126
|
+
const profileRank = new Map(PROFILE_RESTRICTIVENESS_ORDER.map((name, index) => [name, index]));
|
|
117
127
|
|
|
118
128
|
// Sort profiles by number of tools (most restrictive first)
|
|
119
129
|
const candidates = Object.entries(policy.profiles)
|
|
120
130
|
.filter(([, tools]) => tools.includes(toolName))
|
|
121
|
-
.sort((a, b) =>
|
|
131
|
+
.sort((a, b) => {
|
|
132
|
+
const sizeDelta = a[1].length - b[1].length;
|
|
133
|
+
if (sizeDelta !== 0) return sizeDelta;
|
|
134
|
+
return (profileRank.get(a[0]) ?? Number.MAX_SAFE_INTEGER)
|
|
135
|
+
- (profileRank.get(b[0]) ?? Number.MAX_SAFE_INTEGER);
|
|
136
|
+
});
|
|
122
137
|
|
|
123
138
|
if (candidates.length === 0) return null;
|
|
124
139
|
return candidates[0][0]; // most restrictive profile that has this tool
|