Upgrade request analysis routing

This commit is contained in:
Andraxion 2026-06-27 01:44:11 -04:00
parent 1cd446bae8
commit db3e080640
19 changed files with 1520 additions and 66 deletions

View file

@ -7,6 +7,10 @@ const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const kbRoot = path.join(repoRoot, "docs", "kb");
const requestTagCatalogPath = path.join(kbRoot, "tags.json");
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
@ -253,6 +257,45 @@ async function loadRequestTagCatalog() {
return tags;
}
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,
};
}
function buildRequestTagLookup(tagDefinitions) {
return new Map(
(Array.isArray(tagDefinitions) ? tagDefinitions : []).flatMap((entry) => [
@ -388,21 +431,68 @@ async function loadKnowledgeBase() {
const systemsIndex = JSON.parse(await fs.readFile(systemsIndexPath, "utf8"));
const requestSchema = JSON.parse(await fs.readFile(requestSchemaPath, "utf8"));
const requestTagDefinitions = await loadRequestTagCatalog();
const terminology = await loadTerminologyCatalog();
const moduleIndex = await loadModuleIndex();
const docsById = new Map();
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);
}
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,
requestTagDefinitions,
requestTagLookup: buildRequestTagLookup(requestTagDefinitions),
terminology,
moduleIndex,
docsById,
moduleDocsById,
editorCapabilitiesText: await fs.readFile(editorCapabilitiesPath, "utf8").catch(() => ""),
queueWorkflowText: await fs.readFile(queueWorkflowPath, "utf8").catch(() => ""),
};
}
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,
@ -417,15 +507,52 @@ function buildSystemSearchText(system, docText) {
return parts.join(" ").toLowerCase();
}
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();
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);
let score = 0;
let score = terminologyBoostBySystemId.get(String(system.id || "").trim()) || 0;
for (const token of queryTokens) {
if (corpus.includes(token)) {
score += 2;
@ -454,30 +581,280 @@ function pickRelevantSystems(kb, requestText, limit = 4) {
return ranked.slice(0, limit);
}
function buildPrompt(request, relevantSystems, schema, kb) {
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);
const tagCatalogSummary = Array.isArray(kb?.requestTagDefinitions)
? kb.requestTagDefinitions.map((entry) => `- ${entry.label}: ${entry.description || ""}`.trim()).join("\n")
: "";
const systemDocs = relevantSystems.map(({ system, docText }) => {
return [
`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 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.",
"Ground your decisions in the provided KB systems only.",
"Ground your decisions in the provided KB systems and modules only.",
"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.",
"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\".",
@ -490,14 +867,23 @@ function buildPrompt(request, relevantSystems, schema, kb) {
"Raw submission:",
request.sourceText,
"",
"Routing summary:",
JSON.stringify(routing, null, 2),
"",
"Return JSON matching this schema:",
schemaSummary,
"",
"Standardized tags you may use:",
tagCatalogSummary,
"",
"Capability shorthand:",
kbContext.editorCapabilitiesText || "(none)",
"",
"Relevant KB systems:",
systemDocs,
systemDocs || "(none)",
"",
"Relevant KB modules:",
moduleDocs || "(none)",
].join("\n"),
},
];
@ -541,7 +927,7 @@ function readMessageContent(messageContent) {
return "";
}
async function callModelApi(config, messages) {
async function callModelApi(config, messages, options = {}) {
const headers = {
"Content-Type": "application/json",
};
@ -550,8 +936,8 @@ async function callModelApi(config, messages) {
}
const requestBody = {
model: config.model,
temperature: 0.2,
max_tokens: config.maxTokens,
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") {
@ -592,11 +978,13 @@ async function processLauncherRequestAnalysis(config, requestId, body) {
}
function shouldPromoteAnalysis(result, config) {
return result.items.length > 0 && result.items.every((item) => (
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
));
));
}
async function markRequestProcessing(config, request) {
@ -635,6 +1023,7 @@ async function markRequestReview(config, request, result) {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
confidence: result.confidence,
routing: result.routing,
items: result.items,
},
});
@ -648,22 +1037,100 @@ async function promoteRequest(config, request, result) {
submissionId: request.id,
sourceTextSnapshot: request.sourceText,
confidence: result.confidence,
routing: result.routing,
items: result.items,
},
});
}
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) {
const relevantSystems = pickRelevantSystems(kb, request.sourceText, 4);
const systemNames = relevantSystems.map(({ system }) => system.name);
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);
}
const prompt = buildPrompt(request, relevantSystems, kb.requestSchema, kb);
const modelResult = await callModelApi(config, prompt);
const normalizedResult = normalizeAnalysisResult(modelResult, request, relevantSystems.map((entry) => entry.system), kb);
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) {