Worldshaper/scripts/request-analysis-worker.mjs

1224 lines
48 KiB
JavaScript
Raw Permalink Normal View History

import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const kbRoot = path.join(repoRoot, "docs", "kb");
2026-06-27 01:12:35 -04:00
const requestTagCatalogPath = path.join(kbRoot, "tags.json");
2026-06-27 01:44:11 -04:00
const terminologyCatalogPath = path.join(kbRoot, "terminology.json");
const moduleIndexPath = path.join(kbRoot, "modules.json");
const editorCapabilitiesPath = path.join(kbRoot, "editor-capabilities.md");
const queueWorkflowPath = path.join(kbRoot, "queue-workflow.md");
const DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com";
const DEFAULT_DEEPSEEK_MODEL = "deepseek-v4-flash";
const DEFAULT_PROVIDER = process.env.REQUEST_ANALYZER_PROVIDER
|| (String(process.env.DEEPSEEK_API_KEY || "").trim() ? "deepseek" : "openai-compatible");
const DEFAULTS = {
apiBase: process.env.REQUEST_ANALYZER_API_BASE || `http://127.0.0.1:${process.env.PORT || "5180"}`,
provider: DEFAULT_PROVIDER,
modelBaseUrl: process.env.REQUEST_ANALYZER_MODEL_BASE_URL || "",
model: process.env.REQUEST_ANALYZER_MODEL || "",
apiKey: process.env.REQUEST_ANALYZER_API_KEY || process.env.DEEPSEEK_API_KEY || "",
2026-06-27 00:51:20 -04:00
adminPassword: process.env.REQUEST_ANALYZER_ADMIN_PASSWORD || process.env.LAUNCHER_ADMIN_PASSWORD || "",
limit: Number(process.env.REQUEST_ANALYZER_LIMIT || 5),
promoteThreshold: Number(process.env.REQUEST_ANALYZER_PROMOTE_THRESHOLD || 0.85),
maxTokens: Math.max(512, Number(process.env.REQUEST_ANALYZER_MAX_TOKENS || 4000)),
thinking: String(process.env.REQUEST_ANALYZER_THINKING || "disabled").trim().toLowerCase() === "enabled"
? "enabled"
: "disabled",
poll: false,
intervalMs: Number(process.env.REQUEST_ANALYZER_INTERVAL_MS || 30000),
dryRun: false,
requestId: "",
};
function printHelp() {
console.log(`Worldshaper request analysis worker
Usage:
npm run analyze:requests -- [options]
Options:
--provider <name> Model provider: deepseek or openai-compatible.
--request-id <id> Analyze one pending request only.
--limit <n> Maximum pending requests to process in one pass.
--poll Keep polling for new pending requests.
--interval-ms <ms> Poll interval when --poll is enabled.
--dry-run Do not write anything back to the API.
--api-base <url> Worldshaper API base URL.
--model-base-url <url> Model API base URL. Defaults to DeepSeek when provider=deepseek.
--model <id> Model id to use. Defaults to ${DEFAULT_DEEPSEEK_MODEL} for DeepSeek.
--api-key <key> API key for the model endpoint.
--max-tokens <n> Max output tokens for the model response.
--thinking <mode> DeepSeek thinking mode: enabled or disabled.
--promote-threshold <n> Minimum per-item confidence for auto-promotion.
--help Show this message.
Environment variables:
REQUEST_ANALYZER_API_BASE
REQUEST_ANALYZER_PROVIDER
REQUEST_ANALYZER_MODEL_BASE_URL
REQUEST_ANALYZER_MODEL
REQUEST_ANALYZER_API_KEY
2026-06-27 00:51:20 -04:00
REQUEST_ANALYZER_ADMIN_PASSWORD
REQUEST_ANALYZER_MAX_TOKENS
REQUEST_ANALYZER_THINKING
REQUEST_ANALYZER_LIMIT
REQUEST_ANALYZER_INTERVAL_MS
REQUEST_ANALYZER_PROMOTE_THRESHOLD
DEEPSEEK_API_KEY
2026-06-27 00:51:20 -04:00
LAUNCHER_ADMIN_PASSWORD
Notes:
- DeepSeek uses ${DEFAULT_DEEPSEEK_BASE_URL}/chat/completions.
- DeepSeek JSON mode is enabled automatically for this worker.
- Run the Worldshaper API server first so the worker can read and patch requests.
`);
}
function parseArgs(argv) {
const config = { ...DEFAULTS };
for (let index = 0; index < argv.length; index += 1) {
const arg = String(argv[index] || "").trim();
if (!arg) {
continue;
}
if (arg === "--help" || arg === "-h") {
config.help = true;
continue;
}
if (arg === "--provider") {
config.provider = String(argv[index + 1] || "").trim().toLowerCase() || config.provider;
index += 1;
continue;
}
if (arg === "--poll") {
config.poll = true;
continue;
}
if (arg === "--dry-run") {
config.dryRun = true;
continue;
}
if (arg === "--request-id") {
config.requestId = String(argv[index + 1] || "").trim();
index += 1;
continue;
}
if (arg === "--limit") {
config.limit = Math.max(1, Math.floor(Number(argv[index + 1]) || config.limit));
index += 1;
continue;
}
if (arg === "--interval-ms") {
config.intervalMs = Math.max(1000, Math.floor(Number(argv[index + 1]) || config.intervalMs));
index += 1;
continue;
}
if (arg === "--api-base") {
config.apiBase = String(argv[index + 1] || "").trim() || config.apiBase;
index += 1;
continue;
}
if (arg === "--model-base-url") {
config.modelBaseUrl = String(argv[index + 1] || "").trim() || config.modelBaseUrl;
index += 1;
continue;
}
if (arg === "--model") {
config.model = String(argv[index + 1] || "").trim() || config.model;
index += 1;
continue;
}
if (arg === "--api-key") {
config.apiKey = String(argv[index + 1] || "").trim() || config.apiKey;
index += 1;
continue;
}
if (arg === "--max-tokens") {
config.maxTokens = Math.max(512, Math.floor(Number(argv[index + 1]) || config.maxTokens));
index += 1;
continue;
}
if (arg === "--thinking") {
const thinkingValue = String(argv[index + 1] || "").trim().toLowerCase();
config.thinking = thinkingValue === "enabled" ? "enabled" : "disabled";
index += 1;
continue;
}
if (arg === "--promote-threshold") {
const parsed = Number(argv[index + 1]);
if (Number.isFinite(parsed)) {
config.promoteThreshold = Math.max(0, Math.min(1, parsed));
}
index += 1;
continue;
}
throw new Error(`Unknown option: ${arg}`);
}
return config;
}
function finalizeConfig(config) {
const provider = String(config.provider || "").trim().toLowerCase();
const nextConfig = {
...config,
provider: provider === "deepseek" ? "deepseek" : "openai-compatible",
};
if (!String(nextConfig.modelBaseUrl || "").trim()) {
nextConfig.modelBaseUrl = nextConfig.provider === "deepseek"
? DEFAULT_DEEPSEEK_BASE_URL
: "http://127.0.0.1:1234/v1";
}
if (!String(nextConfig.model || "").trim() && nextConfig.provider === "deepseek") {
nextConfig.model = DEFAULT_DEEPSEEK_MODEL;
}
return nextConfig;
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function withTrailingSlash(value) {
return String(value || "").endsWith("/") ? String(value || "") : `${String(value || "")}/`;
}
function buildUrl(base, pathname) {
return new URL(String(pathname || "").replace(/^\//, ""), withTrailingSlash(base)).toString();
}
async function fetchJson(url, init = {}) {
const response = await fetch(url, init);
if (!response.ok) {
const responseText = await response.text().catch(() => "");
throw new Error(`${response.status} ${response.statusText}${responseText ? `: ${responseText.slice(0, 240)}` : ""}`);
}
return response.json();
}
2026-06-27 00:51:20 -04:00
function buildAdminHeaders(config, baseHeaders = {}) {
const nextHeaders = {
...baseHeaders,
};
const adminPassword = String(config?.adminPassword || "").trim();
if (adminPassword) {
nextHeaders["x-worldshaper-admin-password"] = adminPassword;
}
return nextHeaders;
}
function tokenize(value) {
return String(value || "")
.toLowerCase()
.split(/[^a-z0-9_/-]+/i)
.map((entry) => entry.trim())
.filter((entry) => entry.length >= 2);
}
function uniqueStrings(values) {
const seen = new Set();
const next = [];
for (const value of Array.isArray(values) ? values : []) {
const normalized = String(value || "").replace(/\s+/g, " ").trim();
const key = normalized.toLowerCase();
if (!normalized || seen.has(key)) {
continue;
}
seen.add(key);
next.push(normalized);
}
return next;
}
2026-06-27 01:12:35 -04:00
function normalizeRequestTagLookupValue(value) {
return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
}
async function loadRequestTagCatalog() {
const payload = JSON.parse(await fs.readFile(requestTagCatalogPath, "utf8"));
const tags = Array.isArray(payload?.tags)
? payload.tags
.map((entry) => ({
id: String(entry?.id || "").trim(),
label: String(entry?.label || "").trim(),
aliases: Array.isArray(entry?.aliases) ? entry.aliases.map((alias) => String(alias || "").trim()).filter(Boolean) : [],
}))
.filter((entry) => entry.id && entry.label)
: [];
if (tags.length === 0) {
throw new Error("Request tag catalog did not contain any valid tags.");
}
return tags;
}
2026-06-27 01:44:11 -04:00
async function loadTerminologyCatalog() {
const payload = JSON.parse(await fs.readFile(terminologyCatalogPath, "utf8"));
const terms = Array.isArray(payload?.terms)
? payload.terms
.map((entry) => ({
canonical: String(entry?.canonical || "").trim(),
aliases: Array.isArray(entry?.aliases) ? entry.aliases.map((alias) => String(alias || "").trim()).filter(Boolean) : [],
tags: Array.isArray(entry?.tags) ? entry.tags.map((tag) => String(tag || "").trim()).filter(Boolean) : [],
systemIds: Array.isArray(entry?.systemIds) ? entry.systemIds.map((systemId) => String(systemId || "").trim()).filter(Boolean) : [],
}))
.filter((entry) => entry.canonical)
: [];
return {
schemaVersion: Number(payload?.schemaVersion) || 1,
terms,
};
}
async function loadModuleIndex() {
const payload = JSON.parse(await fs.readFile(moduleIndexPath, "utf8"));
const modules = Array.isArray(payload?.modules)
? payload.modules
.map((entry) => ({
id: String(entry?.id || "").trim(),
name: String(entry?.name || "").trim(),
docPath: String(entry?.docPath || "").trim(),
tags: Array.isArray(entry?.tags) ? entry.tags.map((tag) => String(tag || "").trim()).filter(Boolean) : [],
systemIds: Array.isArray(entry?.systemIds) ? entry.systemIds.map((systemId) => String(systemId || "").trim()).filter(Boolean) : [],
aliases: Array.isArray(entry?.aliases) ? entry.aliases.map((alias) => String(alias || "").trim()).filter(Boolean) : [],
priority: String(entry?.priority || "medium").trim().toLowerCase(),
}))
.filter((entry) => entry.id && entry.name && entry.docPath)
: [];
return {
schemaVersion: Number(payload?.schemaVersion) || 1,
modules,
};
}
2026-06-27 01:12:35 -04:00
function buildRequestTagLookup(tagDefinitions) {
return new Map(
(Array.isArray(tagDefinitions) ? tagDefinitions : []).flatMap((entry) => [
[normalizeRequestTagLookupValue(entry.label), entry.label],
...(Array.isArray(entry.aliases) ? entry.aliases.map((alias) => [normalizeRequestTagLookupValue(alias), entry.label]) : []),
]),
);
}
function normalizeRequestTags(values, tagLookup, fallback = []) {
const normalized = uniqueStrings(Array.isArray(values) ? values : [])
.map((entry) => tagLookup.get(normalizeRequestTagLookupValue(entry)) || "")
.filter(Boolean);
if (normalized.length > 0) {
return uniqueStrings(normalized);
}
return uniqueStrings(Array.isArray(fallback) ? fallback : [])
.map((entry) => tagLookup.get(normalizeRequestTagLookupValue(entry)) || "")
.filter(Boolean);
}
function clampConfidence(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return null;
}
return Math.max(0, Math.min(1, parsed));
}
function normalizeItemStatusRecommendation(value) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "active" || normalized === "duplicate" || normalized === "blocked" || normalized === "needs_review") {
return normalized;
}
return "needs_review";
}
function normalizeProblemType(value) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "feature" || normalized === "bug" || normalized === "workflow" || normalized === "performance" || normalized === "ux" || normalized === "content") {
return normalized;
}
return "unknown";
}
function buildFallbackTitle(text, fallback = "Pending request") {
const normalized = String(text || "").replace(/\s+/g, " ").trim();
if (!normalized) {
return fallback;
}
const firstSentence = normalized.split(/[\r\n.!?]+/).map((entry) => entry.trim()).find(Boolean) || normalized;
return firstSentence.length > 72 ? `${firstSentence.slice(0, 69).trim()}...` : firstSentence;
}
2026-06-27 01:12:35 -04:00
function normalizeAnalysisItem(rawItem, index = 0, request = null, relevantSystems = [], kb = null) {
const source = rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)
? rawItem
: null;
if (!source) {
return null;
}
const fallbackText = String(source.rawExcerpt || request?.sourceText || "").trim();
const title = String(source.title || "").trim() || buildFallbackTitle(fallbackText, `Analyzed request ${index + 1}`);
const parsedInterpretation = String(source.parsedInterpretation || source.summary || "").trim();
const implementationApproach = String(source.implementationApproach || source.implementationNotes || "").trim();
if (!title || !parsedInterpretation || !implementationApproach) {
return null;
}
const primaryCategory = String(source.primaryCategory || source.category || "").trim()
|| String(relevantSystems[0]?.name || "Unsorted");
const affectedSystems = uniqueStrings(source.affectedSystems);
2026-06-27 01:12:35 -04:00
const defaultTags = [
primaryCategory,
...affectedSystems,
2026-06-27 01:12:35 -04:00
];
const tags = normalizeRequestTags(
Array.isArray(source.tags) ? source.tags : defaultTags,
kb?.requestTagLookup || new Map(),
defaultTags,
);
const reviewRationale = String(source.reviewRationale || source.reviewReason || source.notes || "").trim();
const reviewOptions = uniqueStrings(source.reviewOptions);
return {
title,
primaryCategory,
2026-06-27 01:12:35 -04:00
tags: tags.length > 0 ? tags : ["General"],
statusRecommendation: normalizeItemStatusRecommendation(source.statusRecommendation || source.status),
parsedInterpretation,
implementationApproach,
affectedSystems,
affectedFiles: uniqueStrings(source.affectedFiles),
problemType: normalizeProblemType(source.problemType),
rawExcerpt: String(source.rawExcerpt || "").trim(),
confidence: clampConfidence(source.confidence),
2026-06-27 01:12:35 -04:00
reviewRationale,
reviewOptions,
notes: String(source.notes || "").trim(),
};
}
2026-06-27 01:12:35 -04:00
function normalizeAnalysisResult(rawResult, request, relevantSystems, kb = null) {
const source = rawResult && typeof rawResult === "object" && !Array.isArray(rawResult)
? rawResult
: (Array.isArray(rawResult) ? { items: rawResult } : null);
if (!source) {
throw new Error("Model response was not a JSON object.");
}
const items = (Array.isArray(source.items) ? source.items : [])
2026-06-27 01:12:35 -04:00
.map((item, index) => normalizeAnalysisItem(item, index, request, relevantSystems, kb))
.filter(Boolean);
if (items.length === 0) {
throw new Error("Model response did not contain any valid request items.");
}
const finiteConfidences = items.map((item) => item.confidence).filter((value) => Number.isFinite(value));
const averageConfidence = finiteConfidences.length > 0
? finiteConfidences.reduce((total, value) => total + value, 0) / finiteConfidences.length
: null;
const minimumConfidence = finiteConfidences.length > 0
? Math.min(...finiteConfidences)
: null;
return {
submissionId: String(source.submissionId || request.id || "").trim() || request.id,
sourceText: String(source.sourceText || request.sourceText || "").trim() || request.sourceText,
confidence: clampConfidence(source.confidence) ?? averageConfidence,
minimumConfidence,
items,
};
}
async function loadKnowledgeBase() {
const systemsIndexPath = path.join(kbRoot, "systems.json");
const requestSchemaPath = path.join(kbRoot, "request-analysis-schema.json");
const systemsIndex = JSON.parse(await fs.readFile(systemsIndexPath, "utf8"));
const requestSchema = JSON.parse(await fs.readFile(requestSchemaPath, "utf8"));
2026-06-27 01:12:35 -04:00
const requestTagDefinitions = await loadRequestTagCatalog();
2026-06-27 01:44:11 -04:00
const terminology = await loadTerminologyCatalog();
const moduleIndex = await loadModuleIndex();
const docsById = new Map();
2026-06-27 01:44:11 -04:00
const moduleDocsById = new Map();
for (const system of Array.isArray(systemsIndex.systems) ? systemsIndex.systems : []) {
const docPath = path.join(repoRoot, String(system.docPath || "").replace(/\//g, path.sep));
const docText = await fs.readFile(docPath, "utf8").catch(() => "");
docsById.set(String(system.id || ""), docText);
}
2026-06-27 01:44:11 -04:00
for (const moduleEntry of Array.isArray(moduleIndex.modules) ? moduleIndex.modules : []) {
const docPath = path.join(repoRoot, String(moduleEntry.docPath || "").replace(/\//g, path.sep));
const docText = await fs.readFile(docPath, "utf8").catch(() => "");
moduleDocsById.set(String(moduleEntry.id || ""), docText);
}
return {
systemsIndex,
requestSchema,
2026-06-27 01:12:35 -04:00
requestTagDefinitions,
requestTagLookup: buildRequestTagLookup(requestTagDefinitions),
2026-06-27 01:44:11 -04:00
terminology,
moduleIndex,
docsById,
2026-06-27 01:44:11 -04:00
moduleDocsById,
editorCapabilitiesText: await fs.readFile(editorCapabilitiesPath, "utf8").catch(() => ""),
queueWorkflowText: await fs.readFile(queueWorkflowPath, "utf8").catch(() => ""),
};
}
2026-06-27 01:44:11 -04:00
function normalizeAmbiguityLevel(value) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "low" || normalized === "medium" || normalized === "high") {
return normalized;
}
return "medium";
}
function normalizeSystemIds(values, kb, fallback = []) {
const knownIds = new Set((Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).map((system) => String(system.id || "").trim()));
const requested = uniqueStrings(Array.isArray(values) ? values : [])
.map((entry) => String(entry || "").trim())
.filter((entry) => knownIds.has(entry));
if (requested.length > 0) {
return requested;
}
return uniqueStrings(Array.isArray(fallback) ? fallback : [])
.map((entry) => String(entry || "").trim())
.filter((entry) => knownIds.has(entry));
}
function normalizeModuleIds(values, kb, fallback = []) {
const knownIds = new Set((Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : []).map((moduleEntry) => String(moduleEntry.id || "").trim()));
const requested = uniqueStrings(Array.isArray(values) ? values : [])
.map((entry) => String(entry || "").trim())
.filter((entry) => knownIds.has(entry));
if (requested.length > 0) {
return requested;
}
return uniqueStrings(Array.isArray(fallback) ? fallback : [])
.map((entry) => String(entry || "").trim())
.filter((entry) => knownIds.has(entry));
}
function buildSystemSearchText(system, docText) {
const parts = [
system?.id,
system?.name,
...(Array.isArray(system?.aliases) ? system.aliases : []),
...(Array.isArray(system?.tags) ? system.tags : []),
...(Array.isArray(system?.uiSurfaces) ? system.uiSurfaces : []),
...(Array.isArray(system?.keyFiles) ? system.keyFiles : []),
...(Array.isArray(system?.apiEndpoints) ? system.apiEndpoints : []),
docText,
];
return parts.join(" ").toLowerCase();
}
2026-06-27 01:44:11 -04:00
function buildModuleSearchText(moduleEntry, docText) {
const parts = [
moduleEntry?.id,
moduleEntry?.name,
...(Array.isArray(moduleEntry?.aliases) ? moduleEntry.aliases : []),
...(Array.isArray(moduleEntry?.tags) ? moduleEntry.tags : []),
...(Array.isArray(moduleEntry?.systemIds) ? moduleEntry.systemIds : []),
docText,
];
return parts.join(" ").toLowerCase();
}
function collectTerminologyMatches(kb, requestText) {
const requestLower = String(requestText || "").toLowerCase();
return (Array.isArray(kb?.terminology?.terms) ? kb.terminology.terms : [])
.map((term) => {
const phrases = [term.canonical, ...(Array.isArray(term.aliases) ? term.aliases : [])]
.map((entry) => String(entry || "").trim().toLowerCase())
.filter(Boolean);
const matched = phrases.filter((phrase) => requestLower.includes(phrase));
return matched.length > 0
? {
term,
matched,
}
: null;
})
.filter(Boolean);
}
function pickRelevantSystems(kb, requestText, limit = 4) {
const systems = Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : [];
const queryTokens = tokenize(requestText);
const requestLower = String(requestText || "").toLowerCase();
2026-06-27 01:44:11 -04:00
const terminologyMatches = collectTerminologyMatches(kb, requestText);
const terminologyBoostBySystemId = new Map();
terminologyMatches.forEach(({ term, matched }) => {
(Array.isArray(term.systemIds) ? term.systemIds : []).forEach((systemId) => {
terminologyBoostBySystemId.set(systemId, (terminologyBoostBySystemId.get(systemId) || 0) + (matched.length * 4));
});
});
const ranked = systems
.map((system) => {
const docText = kb.docsById.get(String(system.id || "")) || "";
const corpus = buildSystemSearchText(system, docText);
2026-06-27 01:44:11 -04:00
let score = terminologyBoostBySystemId.get(String(system.id || "").trim()) || 0;
for (const token of queryTokens) {
if (corpus.includes(token)) {
score += 2;
}
}
for (const alias of Array.isArray(system.aliases) ? system.aliases : []) {
const normalizedAlias = String(alias || "").trim().toLowerCase();
if (normalizedAlias && requestLower.includes(normalizedAlias)) {
score += 5;
}
}
if (String(system.priorityForRequestTriage || "").trim().toLowerCase() === "high") {
score += 1;
}
return {
system,
docText,
score,
};
})
.sort((left, right) => right.score - left.score || String(left.system?.name || "").localeCompare(String(right.system?.name || "")));
const positive = ranked.filter((entry) => entry.score > 0).slice(0, limit);
if (positive.length > 0) {
return positive;
}
return ranked.slice(0, limit);
}
2026-06-27 01:44:11 -04:00
function pickRelevantModules(kb, requestText, routing, limit = 4) {
const modules = Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : [];
const queryTokens = tokenize(requestText);
const requestLower = String(requestText || "").toLowerCase();
const suggestedTags = new Set(Array.isArray(routing?.suggestedTags) ? routing.suggestedTags : []);
const suggestedSystems = new Set(Array.isArray(routing?.suggestedSystems) ? routing.suggestedSystems : []);
const ranked = modules
.map((moduleEntry) => {
const docText = kb.moduleDocsById.get(String(moduleEntry.id || "")) || "";
const corpus = buildModuleSearchText(moduleEntry, docText);
let score = 0;
for (const token of queryTokens) {
if (corpus.includes(token)) {
score += 2;
}
}
for (const alias of Array.isArray(moduleEntry.aliases) ? moduleEntry.aliases : []) {
const normalizedAlias = String(alias || "").trim().toLowerCase();
if (normalizedAlias && requestLower.includes(normalizedAlias)) {
score += 5;
}
}
for (const tag of Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []) {
if (suggestedTags.has(tag)) {
score += 4;
}
}
for (const systemId of Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []) {
if (suggestedSystems.has(systemId)) {
score += 4;
}
}
if (String(moduleEntry.priority || "medium") === "high") {
score += 1;
}
return {
moduleEntry,
docText,
score,
};
})
.sort((left, right) => right.score - left.score || String(left.moduleEntry?.name || "").localeCompare(String(right.moduleEntry?.name || "")));
const positive = ranked.filter((entry) => entry.score > 0).slice(0, limit);
if (positive.length > 0) {
return positive;
}
return ranked.slice(0, limit);
}
function deriveHeuristicRouting(kb, requestText) {
const terminologyMatches = collectTerminologyMatches(kb, requestText);
const relevantSystems = pickRelevantSystems(kb, requestText, 4);
const matchedTerms = uniqueStrings(
terminologyMatches.flatMap(({ matched }) => matched),
);
const suggestedTags = uniqueStrings([
...terminologyMatches.flatMap(({ term }) => Array.isArray(term.tags) ? term.tags : []),
...relevantSystems.flatMap(({ system }) => Array.isArray(system.tags) ? system.tags : []),
]);
const suggestedSystems = uniqueStrings([
...terminologyMatches.flatMap(({ term }) => Array.isArray(term.systemIds) ? term.systemIds : []),
...relevantSystems.map(({ system }) => String(system.id || "").trim()),
]);
const suggestedModules = pickRelevantModules(kb, requestText, {
suggestedTags,
suggestedSystems,
}, 4).map(({ moduleEntry }) => String(moduleEntry.id || "").trim());
const normalizedText = String(requestText || "").replace(/\s+/g, " ").trim();
const tokenCount = tokenize(normalizedText).length;
const genericPatterns = [
/\b(make|improve|fix|upgrade)\b.+\b(game|editor|worldshaper|site|launcher|it)\b/i,
/^\s*(better|more|faster|cooler|good)\s*!?\s*$/i,
/^\s*add\s+[a-z0-9_-]+\s*!?\s*$/i,
];
const isBroad = genericPatterns.some((pattern) => pattern.test(normalizedText)) || tokenCount <= 4;
const ambiguity = isBroad
? "high"
: (matchedTerms.length === 0 || suggestedSystems.length > 3 ? "medium" : "low");
const displaySystemNames = suggestedSystems
.map((systemId) => (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).find((system) => String(system.id || "").trim() === systemId)?.name || systemId)
.slice(0, 4);
const possibleDirections = uniqueStrings([
suggestedTags.includes("Graphics Painter") ? "Treat it as an asset-editing request inside the Graphics Painter." : "",
suggestedTags.includes("Tiling") ? "Treat it as map-grid painting or placement behavior on the tile canvas." : "",
suggestedTags.includes("Content") ? "Treat it as a new asset or content-record request that needs catalog support." : "",
suggestedTags.includes("Rendering") || suggestedTags.includes("Worlds") ? "Treat it as a runtime-facing request that may need renderer or world metadata support." : "",
suggestedTags.includes("Chunks") ? "Treat it as a chunk or world-scale placement workflow." : "",
]).slice(0, 4);
return {
summary: displaySystemNames.length > 0
? `Likely touches ${displaySystemNames.join(", ")}.`
: "Broad request with no single obvious subsystem match yet.",
ambiguity,
matchedTerms,
suggestedTags: suggestedTags.length > 0 ? suggestedTags.slice(0, 6) : ["General"],
suggestedSystems: suggestedSystems.slice(0, 4),
suggestedModules,
rationale: matchedTerms.length > 0
? `Matched terminology: ${matchedTerms.join(", ")}.`
: "Used broad alias and system matching because the submission did not name an exact editor surface.",
possibleDirections: possibleDirections.length > 0
? possibleDirections
: ["Clarify which editor surface or runtime behavior the request should target first."],
};
}
function normalizeRoutingResult(rawResult, kb, requestText, fallbackRouting) {
const source = rawResult && typeof rawResult === "object" && !Array.isArray(rawResult)
? rawResult
: {};
const suggestedTags = normalizeRequestTags(
Array.isArray(source.suggestedTags) ? source.suggestedTags : source.tags,
kb?.requestTagLookup || new Map(),
fallbackRouting?.suggestedTags || ["General"],
);
const suggestedSystems = normalizeSystemIds(
Array.isArray(source.suggestedSystems) ? source.suggestedSystems : source.suggestedSystemIds,
kb,
fallbackRouting?.suggestedSystems || [],
);
const suggestedModules = normalizeModuleIds(
Array.isArray(source.suggestedModules) ? source.suggestedModules : source.suggestedModuleIds,
kb,
fallbackRouting?.suggestedModules || [],
);
const matchedTerms = uniqueStrings(source.matchedTerms || source.terms || fallbackRouting?.matchedTerms || []);
const possibleDirections = uniqueStrings(source.possibleDirections || fallbackRouting?.possibleDirections || []);
const summary = String(source.summary || source.routingSummary || fallbackRouting?.summary || "").trim()
|| `Likely touches ${suggestedTags.join(", ")}.`;
const rationale = String(source.rationale || source.reviewRationale || fallbackRouting?.rationale || "").trim()
|| "Routing used terminology and KB alias matching.";
const ambiguity = normalizeAmbiguityLevel(source.ambiguity || fallbackRouting?.ambiguity || "");
const kbSections = uniqueStrings([
...suggestedSystems.map((systemId) => {
const system = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).find((entry) => String(entry.id || "").trim() === systemId);
return String(system?.docPath || "").trim();
}),
...suggestedModules.map((moduleId) => {
const moduleEntry = (Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : []).find((entry) => String(entry.id || "").trim() === moduleId);
return String(moduleEntry?.docPath || "").trim();
}),
].filter(Boolean));
return {
summary,
ambiguity,
matchedTerms,
suggestedTags: suggestedTags.length > 0 ? suggestedTags : ["General"],
suggestedSystems,
suggestedModules,
rationale,
possibleDirections: possibleDirections.length > 0
? possibleDirections
: (fallbackRouting?.possibleDirections || ["Clarify which part of the editor or runtime should own this request."]),
kbSections,
sourceText: String(requestText || "").trim(),
};
}
function buildRoutingPrompt(request, kb, heuristicRouting) {
const systemSummaries = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : [])
.map((system) => `- ${system.id}: ${system.name} | tags=${(Array.isArray(system.tags) ? system.tags : []).join(", ")} | aliases=${(Array.isArray(system.aliases) ? system.aliases : []).join(", ")}`)
.join("\n");
const moduleSummaries = (Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : [])
.map((moduleEntry) => `- ${moduleEntry.id}: ${moduleEntry.name} | tags=${(Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []).join(", ")} | systems=${(Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []).join(", ")}`)
.join("\n");
const terminologySummary = (Array.isArray(kb?.terminology?.terms) ? kb.terminology.terms : [])
.map((term) => `- ${term.canonical}: aliases=${(Array.isArray(term.aliases) ? term.aliases : []).join(", ")} | tags=${(Array.isArray(term.tags) ? term.tags : []).join(", ")} | systems=${(Array.isArray(term.systemIds) ? term.systemIds : []).join(", ")}`)
.join("\n");
return [
{
role: "system",
content: [
"You are the routing pass for Worldshaper request analysis.",
"Map the submission onto likely systems, modules, and standardized tags before the deeper analysis pass runs.",
"Prefer Worldshaper terminology when the user uses adjacent language such as sprite editor, painting tool, recoloring, engine, runtime, map, grid, or chunk.",
"Broad requests must still receive a useful interpretation and possible directions.",
"Do not expose or simulate hidden chain-of-thought.",
"Return only valid JSON with keys: summary, ambiguity, matchedTerms, suggestedTags, suggestedSystems, suggestedModules, rationale, possibleDirections.",
].join("\n"),
},
{
role: "user",
content: [
`Submission id: ${request.id}`,
"Raw submission:",
request.sourceText,
"",
"Standardized tags:",
(Array.isArray(kb?.requestTagDefinitions) ? kb.requestTagDefinitions : []).map((entry) => `- ${entry.label}: ${entry.description || ""}`).join("\n"),
"",
"System index:",
systemSummaries,
"",
"Focused modules:",
moduleSummaries,
"",
"Terminology map:",
terminologySummary,
"",
"Heuristic seed:",
JSON.stringify(heuristicRouting, null, 2),
].join("\n"),
},
];
}
function buildKbContext(kb, requestText, routing) {
const relevantSystems = uniqueStrings([
...normalizeSystemIds(routing?.suggestedSystems, kb, []),
...pickRelevantSystems(kb, requestText, 4).map(({ system }) => String(system.id || "").trim()),
])
.slice(0, 4)
.map((systemId) => {
const system = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : []).find((entry) => String(entry.id || "").trim() === systemId);
return system
? {
system,
docText: kb.docsById.get(systemId) || "",
}
: null;
})
.filter(Boolean);
const relevantModules = uniqueStrings([
...normalizeModuleIds(routing?.suggestedModules, kb, []),
...pickRelevantModules(kb, requestText, routing, 5).map(({ moduleEntry }) => String(moduleEntry.id || "").trim()),
])
.slice(0, 5)
.map((moduleId) => {
const moduleEntry = (Array.isArray(kb?.moduleIndex?.modules) ? kb.moduleIndex.modules : []).find((entry) => String(entry.id || "").trim() === moduleId);
return moduleEntry
? {
moduleEntry,
docText: kb.moduleDocsById.get(moduleId) || "",
}
: null;
})
.filter(Boolean);
return {
systems: relevantSystems,
modules: relevantModules,
editorCapabilitiesText: String(kb?.editorCapabilitiesText || "").trim(),
};
}
function buildAnalysisPrompt(request, routing, kbContext, schema, kb) {
const schemaSummary = JSON.stringify(schema, null, 2);
2026-06-27 01:12:35 -04:00
const tagCatalogSummary = Array.isArray(kb?.requestTagDefinitions)
? kb.requestTagDefinitions.map((entry) => `- ${entry.label}: ${entry.description || ""}`.trim()).join("\n")
: "";
2026-06-27 01:44:11 -04:00
const systemDocs = kbContext.systems.map(({ system, docText }) => [
`System: ${system.name}`,
`System ID: ${system.id}`,
`Tags: ${(Array.isArray(system.tags) ? system.tags : []).join(", ")}`,
`Key files: ${(Array.isArray(system.keyFiles) ? system.keyFiles : []).join(", ")}`,
`API endpoints: ${(Array.isArray(system.apiEndpoints) ? system.apiEndpoints : []).join(", ") || "(none)"}`,
docText,
].join("\n")).join("\n\n---\n\n");
const moduleDocs = kbContext.modules.map(({ moduleEntry, docText }) => [
`Module: ${moduleEntry.name}`,
`Module ID: ${moduleEntry.id}`,
`Tags: ${(Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []).join(", ")}`,
`System IDs: ${(Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []).join(", ")}`,
docText,
].join("\n")).join("\n\n---\n\n");
return [
{
role: "system",
content: [
"You are processing Worldshaper editor requests.",
"Split a submission into one or more atomic requests.",
2026-06-27 01:44:11 -04:00
"Ground your decisions in the provided KB systems and modules only.",
2026-06-27 01:12:35 -04:00
"Use only the standardized tags listed in the provided tag catalog.",
"Do not expose or simulate hidden chain-of-thought. Provide short structured review rationale instead.",
2026-06-27 01:44:11 -04:00
"If a request is broad or ambiguous, still provide the most useful likely interpretation and concrete implementation path you can.",
"Return only valid JSON.",
"Do not wrap the JSON in markdown fences.",
"If you are unsure, lower confidence and use statusRecommendation = \"needs_review\".",
].join("\n"),
},
{
role: "user",
content: [
`Submission id: ${request.id}`,
"Raw submission:",
request.sourceText,
"",
2026-06-27 01:44:11 -04:00
"Routing summary:",
JSON.stringify(routing, null, 2),
"",
"Return JSON matching this schema:",
schemaSummary,
"",
2026-06-27 01:12:35 -04:00
"Standardized tags you may use:",
tagCatalogSummary,
"",
2026-06-27 01:44:11 -04:00
"Capability shorthand:",
kbContext.editorCapabilitiesText || "(none)",
"",
"Relevant KB systems:",
2026-06-27 01:44:11 -04:00
systemDocs || "(none)",
"",
"Relevant KB modules:",
moduleDocs || "(none)",
].join("\n"),
},
];
}
function extractJsonString(text) {
const rawText = String(text || "").trim();
if (!rawText) {
throw new Error("Model returned empty content.");
}
const fencedMatch = rawText.match(/```(?:json)?\s*([\s\S]*?)```/i);
if (fencedMatch && fencedMatch[1]) {
return fencedMatch[1].trim();
}
const firstBrace = rawText.indexOf("{");
const lastBrace = rawText.lastIndexOf("}");
if (firstBrace >= 0 && lastBrace > firstBrace) {
return rawText.slice(firstBrace, lastBrace + 1);
}
throw new Error("Could not find JSON object in model response.");
}
function readMessageContent(messageContent) {
if (typeof messageContent === "string") {
return messageContent;
}
if (Array.isArray(messageContent)) {
return messageContent
.map((part) => {
if (typeof part === "string") {
return part;
}
if (part && typeof part === "object" && typeof part.text === "string") {
return part.text;
}
return "";
})
.filter(Boolean)
.join("\n");
}
return "";
}
2026-06-27 01:44:11 -04:00
async function callModelApi(config, messages, options = {}) {
const headers = {
"Content-Type": "application/json",
};
if (config.apiKey) {
headers.Authorization = `Bearer ${config.apiKey}`;
}
const requestBody = {
model: config.model,
2026-06-27 01:44:11 -04:00
temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.2,
max_tokens: Math.max(256, Math.floor(Number(options.maxTokens) || config.maxTokens)),
messages,
};
if (config.provider === "deepseek") {
requestBody.response_format = { type: "json_object" };
requestBody.thinking = { type: config.thinking };
}
const payload = await fetchJson(buildUrl(config.modelBaseUrl, "chat/completions"), {
method: "POST",
headers,
body: JSON.stringify(requestBody),
});
const text = readMessageContent(payload?.choices?.[0]?.message?.content);
return JSON.parse(extractJsonString(text));
}
async function getLauncherRequests(config) {
return fetchJson(buildUrl(config.apiBase, "/api/launcher-requests"));
}
async function patchLauncherRequest(config, requestId, body) {
return fetchJson(buildUrl(config.apiBase, `/api/launcher-requests/${encodeURIComponent(requestId)}`), {
method: "PATCH",
2026-06-27 00:51:20 -04:00
headers: buildAdminHeaders(config, {
"Content-Type": "application/json",
2026-06-27 00:51:20 -04:00
}),
body: JSON.stringify(body),
});
}
async function processLauncherRequestAnalysis(config, requestId, body) {
return fetchJson(buildUrl(config.apiBase, `/api/launcher-requests/${encodeURIComponent(requestId)}/process-analysis`), {
method: "POST",
2026-06-27 00:51:20 -04:00
headers: buildAdminHeaders(config, {
"Content-Type": "application/json",
2026-06-27 00:51:20 -04:00
}),
body: JSON.stringify(body),
});
}
function shouldPromoteAnalysis(result, config) {
2026-06-27 01:44:11 -04:00
return result.items.length > 0
&& String(result?.routing?.ambiguity || "").trim().toLowerCase() !== "high"
&& result.items.every((item) => (
item.statusRecommendation === "active"
&& Number.isFinite(item.confidence)
&& item.confidence >= config.promoteThreshold
2026-06-27 01:44:11 -04:00
));
}
async function markRequestProcessing(config, request) {
const now = new Date().toISOString();
return patchLauncherRequest(config, request.id, {
analysis: {
...(request.analysis && typeof request.analysis === "object" && !Array.isArray(request.analysis) ? request.analysis : {}),
state: "processing",
model: config.model,
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
createdAt: request.analysis?.createdAt || now,
updatedAt: now,
error: "",
},
});
}
async function markRequestError(config, request, errorMessage) {
return processLauncherRequestAnalysis(config, request.id, {
action: "error",
model: config.model,
error: String(errorMessage || "Unknown analysis failure."),
analysis: {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
},
});
}
async function markRequestReview(config, request, result) {
return processLauncherRequestAnalysis(config, request.id, {
action: "review",
model: config.model,
analysis: {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
confidence: result.confidence,
2026-06-27 01:44:11 -04:00
routing: result.routing,
items: result.items,
},
});
}
async function promoteRequest(config, request, result) {
return processLauncherRequestAnalysis(config, request.id, {
action: "promote",
model: config.model,
analysis: {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
confidence: result.confidence,
2026-06-27 01:44:11 -04:00
routing: result.routing,
items: result.items,
},
});
}
2026-06-27 01:44:11 -04:00
function buildFallbackAnalysisResult(request, routing, kb) {
const primaryTag = Array.isArray(routing?.suggestedTags) && routing.suggestedTags.length > 0
? routing.suggestedTags[0]
: "General";
const primarySystemId = Array.isArray(routing?.suggestedSystems) && routing.suggestedSystems.length > 0
? routing.suggestedSystems[0]
: "";
const primarySystem = (Array.isArray(kb?.systemsIndex?.systems) ? kb.systemsIndex.systems : [])
.find((system) => String(system.id || "").trim() === primarySystemId);
const title = buildFallbackTitle(
request.sourceText,
routing?.ambiguity === "high" ? "Broader editor improvement request" : "Pending request",
);
const parsedInterpretation = routing?.ambiguity === "high"
? `This submission is broad, but it most likely points toward ${primaryTag} work${primarySystem?.name ? ` touching ${primarySystem.name}` : ""}. It needs a narrower decision before it can be promoted cleanly.`
: `This request most likely targets ${primarySystem?.name || primaryTag}, based on the submission language and the matched KB terminology.`;
const implementationApproach = routing?.ambiguity === "high"
? `Turn the request into a smaller scoped follow-up by choosing one direction first: ${(Array.isArray(routing?.possibleDirections) && routing.possibleDirections.length > 0 ? routing.possibleDirections : ["clarify whether this should be content, map editing, or runtime work"]).join(" ")}`.trim()
: `Start from ${primarySystem?.name || primaryTag}, audit the existing workflow there, and then scope the change so the request can be broken into editor-facing behavior, saved data changes, and any runtime handoff that may be required.`;
const reviewOptions = Array.isArray(routing?.possibleDirections) && routing.possibleDirections.length > 0
? routing.possibleDirections
: ["Clarify which part of the editor or runtime should own this request first."];
return {
submissionId: request.id,
sourceText: request.sourceText,
confidence: 0.38,
minimumConfidence: 0.38,
routing,
items: [
{
title,
primaryCategory: primaryTag,
tags: Array.isArray(routing?.suggestedTags) && routing.suggestedTags.length > 0 ? routing.suggestedTags : ["General"],
statusRecommendation: "needs_review",
parsedInterpretation,
implementationApproach,
affectedSystems: primarySystem?.name ? [primarySystem.name] : [],
affectedFiles: [],
problemType: "feature",
rawExcerpt: String(request.sourceText || "").trim(),
confidence: 0.38,
reviewRationale: String(routing?.rationale || "").trim() || "The request is still too broad to auto-promote safely.",
reviewOptions,
notes: String(routing?.summary || "").trim(),
},
],
};
}
async function analyzeRequest(config, kb, request) {
2026-06-27 01:44:11 -04:00
const heuristicRouting = deriveHeuristicRouting(kb, request.sourceText);
const routingPrompt = buildRoutingPrompt(request, kb, heuristicRouting);
let routing = heuristicRouting;
try {
const routingResult = await callModelApi(config, routingPrompt, {
maxTokens: Math.min(config.maxTokens, 900),
temperature: 0.1,
});
routing = normalizeRoutingResult(routingResult, kb, request.sourceText, heuristicRouting);
} catch (error) {
console.warn(` Routing pass fallback for ${request.id}: ${String(error)}`);
routing = normalizeRoutingResult({}, kb, request.sourceText, heuristicRouting);
}
const kbContext = buildKbContext(kb, request.sourceText, routing);
const systemNames = kbContext.systems.map(({ system }) => system.name);
console.log(`Analyzing ${request.id}: ${request.title}`);
console.log(` Systems: ${systemNames.join(", ")}`);
if (!config.dryRun) {
await markRequestProcessing(config, request);
}
2026-06-27 01:44:11 -04:00
const prompt = buildAnalysisPrompt(request, routing, kbContext, kb.requestSchema, kb);
let normalizedResult;
try {
const modelResult = await callModelApi(config, prompt, {
maxTokens: config.maxTokens,
temperature: 0.2,
});
normalizedResult = normalizeAnalysisResult(
modelResult,
request,
kbContext.systems.map((entry) => entry.system),
kb,
);
} catch (error) {
console.warn(` Analysis pass fallback for ${request.id}: ${String(error)}`);
normalizedResult = buildFallbackAnalysisResult(request, routing, kb);
}
normalizedResult.routing = routing;
const action = shouldPromoteAnalysis(normalizedResult, config) ? "promote" : "review";
console.log(` Result: ${normalizedResult.items.length} item(s), action=${action}, confidence=${normalizedResult.confidence ?? "n/a"}`);
if (config.dryRun) {
console.log(JSON.stringify({
requestId: request.id,
action,
result: normalizedResult,
}, null, 2));
return { action, result: normalizedResult };
}
if (action === "promote") {
await promoteRequest(config, request, normalizedResult);
} else {
await markRequestReview(config, request, normalizedResult);
}
return { action, result: normalizedResult };
}
function selectPendingRequests(payload, config) {
const requests = Array.isArray(payload?.requests) ? payload.requests : [];
const pending = requests.filter((request) => String(request?.status || "").trim().toLowerCase() === "pending");
if (config.requestId) {
return pending.filter((request) => String(request?.id || "").trim() === config.requestId);
}
return pending
.filter((request) => {
const analysisState = String(request?.analysis?.state || "").trim().toLowerCase();
return !analysisState || analysisState === "unprocessed";
})
.slice(0, Math.max(1, config.limit));
}
async function runSinglePass(config, kb) {
const payload = await getLauncherRequests(config);
const pendingRequests = selectPendingRequests(payload, config);
if (pendingRequests.length === 0) {
console.log("No eligible pending requests found.");
return 0;
}
let processedCount = 0;
for (const request of pendingRequests) {
try {
await analyzeRequest(config, kb, request);
processedCount += 1;
} catch (error) {
console.error(` Failed to analyze ${request.id}: ${String(error)}`);
if (!config.dryRun) {
try {
await markRequestError(config, request, String(error));
} catch (secondaryError) {
console.error(` Failed to record analysis error for ${request.id}: ${String(secondaryError)}`);
}
}
}
}
return processedCount;
}
async function main() {
const config = finalizeConfig(parseArgs(process.argv.slice(2)));
if (config.help) {
printHelp();
return;
}
if (!config.dryRun && !config.model) {
throw new Error("Missing local model id. Set REQUEST_ANALYZER_MODEL or pass --model.");
}
if (!config.dryRun && !config.apiKey) {
throw new Error(
config.provider === "deepseek"
? "Missing DeepSeek API key. Set DEEPSEEK_API_KEY or REQUEST_ANALYZER_API_KEY."
: "Missing model API key. Set REQUEST_ANALYZER_API_KEY or pass --api-key."
);
}
const kb = await loadKnowledgeBase();
if (config.poll) {
console.log(`Watching pending requests every ${config.intervalMs}ms`);
// eslint-disable-next-line no-constant-condition
while (true) {
await runSinglePass(config, kb);
await sleep(config.intervalMs);
}
}
await runSinglePass(config, kb);
}
main().catch((error) => {
console.error(String(error));
process.exitCode = 1;
});