#!/usr/bin/env node "use strict"; const http = require("http"); const fs = require("fs"); const path = require("path"); const { spawn } = require("child_process"); const { loadConfig, getDefaults } = require("./config-loader"); const { analyzeScene, analyzeSceneDeep } = require("./llm-client"); const { runGenerator, readDirectory, validateSceneIdCandidate } = require("./generator-runner"); let config; let defaults; try { config = loadConfig(); defaults = getDefaults(); if (require.main === module) { console.log(`[config] Loaded from: ${config.configPath}`); console.log(`[config] Project root: ${config.projectRoot}`); } } catch (error) { console.error(`[error] Failed to load config: ${error.message}`); process.exit(1); } const PORT = parseInt(process.env.SG_SCENE_GENERATOR_PORT, 10) || 3210; const HOST = "127.0.0.1"; const MIME_TYPES = { ".html": "text/html; charset=utf-8", ".js": "application/javascript", ".css": "text/css", ".json": "application/json", }; function serveStatic(res, filePath) { const ext = path.extname(filePath); const contentType = MIME_TYPES[ext] || "application/octet-stream"; fs.readFile(filePath, (error, data) => { if (error) { writeJson(res, 404, { error: "Not found" }); return; } res.writeHead(200, { "Content-Type": contentType }); res.end(data); }); } function initSSE(res) { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", "Access-Control-Allow-Origin": "*", }); res.write(":\n"); return res; } function writeSSE(res, event, data) { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } function writeJson(res, statusCode, payload) { res.writeHead(statusCode, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }); res.end(JSON.stringify(payload)); } function parseBody(req) { return new Promise((resolve, reject) => { let body = ""; req.on("data", (chunk) => { body += chunk; }); req.on("end", () => { try { resolve(body ? JSON.parse(body) : {}); } catch (_) { reject(new Error("Invalid JSON")); } }); req.on("error", reject); }); } async function handleAnalyze(req, res) { let body; try { body = await parseBody(req); } catch (_) { writeJson(res, 400, { error: "Invalid JSON body" }); return; } const sourceDir = normalizeInputPath(body.sourceDir); if (!sourceDir) { writeJson(res, 400, { error: "sourceDir is required" }); return; } let dirContents; try { dirContents = readDirectory(sourceDir); } catch (error) { writeJson(res, 400, { error: error.message }); return; } try { const result = await analyzeScene(sourceDir, dirContents, config); writeJson(res, 200, result); } catch (error) { writeJson(res, 502, { error: `LLM analysis failed: ${error.message}`, hint: "You can still enter scene-id and scene-name manually.", }); } } async function handleAnalyzeDeep(req, res) { let body; try { body = await parseBody(req); } catch (_) { writeJson(res, 400, { error: "Invalid JSON body" }); return; } const sourceDir = normalizeInputPath(body.sourceDir); if (!sourceDir) { writeJson(res, 400, { error: "sourceDir is required" }); return; } let dirContents; try { dirContents = readDirectory(sourceDir); } catch (error) { writeJson(res, 400, { error: error.message }); return; } const deterministic = sanitizeSceneIr(dirContents.deterministic || {}); const warnings = []; const sources = { deterministic: true, llm: false, }; let llmSceneIr = null; let llmError = null; if (hasUsableLlmConfig(config)) { try { llmSceneIr = sanitizeSceneIr(await analyzeSceneDeep(sourceDir, dirContents, config)); sources.llm = true; } catch (error) { llmError = error.message; warnings.push(`LLM semantic completion failed: ${error.message}`); } } else { warnings.push("LLM semantic completion skipped because API config is incomplete."); } const merged = mergeSceneIr(deterministic, llmSceneIr, warnings); merged.analysisMeta = { sourceDir, sources, llmError, warnings, deterministicSignals: dirContents.analysisContext?.deterministicSignals || {}, }; console.log( `[analyze-deep] ${merged.sceneId} / ${merged.sceneName} archetype=${merged.workflowArchetype || "unknown"} readiness=${merged.readiness?.level || "?"}` ); writeJson(res, 200, merged); } async function handleGenerate(req, res) { let body; try { body = await parseBody(req); } catch (_) { writeJson(res, 400, { error: "Invalid JSON body" }); return; } const sourceDir = normalizeInputPath(body.sourceDir); const sceneId = stringValue(body.sceneId); const sceneName = stringValue(body.sceneName); const sceneKind = stringValue(body.sceneKind); const targetUrl = stringValue(body.targetUrl); const outputRoot = normalizeInputPath(body.outputRoot); const lessons = normalizeInputPath(body.lessons); const sceneInfoJson = normalizeJsonInput(body.sceneInfoJson); const sceneIrJson = normalizeJsonInput(body.sceneIrJson); if (!sourceDir || !sceneId || !sceneName || !outputRoot) { writeJson(res, 400, { error: "All fields required: sourceDir, sceneId, sceneName, outputRoot", }); return; } const sseWriter = initSSE(res); try { const sceneIr = sceneIrJson ? sanitizeSceneIr(JSON.parse(sceneIrJson)) : null; const blockers = getGenerationBlockers({ sceneIr, sceneId, sceneName, sourceDir, }); if (blockers.length) { writeSSE(sseWriter, "error", { message: `Generation blocked: ${blockers.join(", ")}`, }); writeSSE(sseWriter, "complete", { success: false, blocked: true, blockers, readiness: sceneIr?.readiness || null, workflowArchetype: sceneIr?.workflowArchetype || null, confidence: sceneIr?.confidence || 0, }); return; } await runGenerator( { sourceDir, sceneId, sceneName, sceneKind: sceneKind || null, targetUrl: targetUrl || null, outputRoot, lessons: lessons || null, sceneInfoJson, sceneIrJson, completionMeta: sceneIr ? { readiness: sceneIr.readiness || null, workflowArchetype: sceneIr.workflowArchetype || null, confidence: sceneIr.confidence || 0, } : null, }, sseWriter, config.projectRoot ); } catch (error) { writeSSE(sseWriter, "error", { message: `Server error: ${error.message}` }); } sseWriter.end(); } function handleHealth(req, res) { writeJson(res, 200, { status: "ok", pid: process.pid, configLoaded: true, configPath: config.configPath, projectRoot: config.projectRoot, defaults, }); } function openFolderDialog(defaultPath) { return new Promise((resolve) => { const psScript = ` [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 Add-Type -AssemblyName System.Windows.Forms $dialog = New-Object System.Windows.Forms.FolderBrowserDialog $dialog.Description = "Select a folder" $dialog.ShowNewFolderButton = true ${defaultPath ? `$dialog.SelectedPath = '${defaultPath.replace(/'/g, "''")}'` : ""} if ($dialog.ShowDialog() -eq 'OK') { Write-Output $dialog.SelectedPath } `.trim(); const ps = spawn( "powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", psScript], { windowsHide: true } ); let output = ""; ps.stdout.on("data", (data) => { output += data.toString("utf8"); }); ps.on("close", (code) => { if (code === 0 && output.trim()) { let selected = output.trim(); if (selected.charCodeAt(0) === 0xfeff) { selected = selected.slice(1); } resolve(selected); return; } resolve(null); }); ps.on("error", () => resolve(null)); }); } async function handleSelectFolder(req, res) { let body = {}; try { body = await parseBody(req); } catch (_) {} const selectedPath = await openFolderDialog(body.defaultPath || ""); writeJson(res, 200, { path: selectedPath }); } async function handleSelectFile(req, res) { let body = {}; try { body = await parseBody(req); } catch (_) {} const filter = body.filter || "All files (*.*)|*.*"; const psScript = ` [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 Add-Type -AssemblyName System.Windows.Forms $dialog = New-Object System.Windows.Forms.OpenFileDialog $dialog.Filter = '${filter.replace(/'/g, "''")}' $dialog.Title = "Select a file" ${body.defaultPath ? `$dialog.InitialDirectory = '${body.defaultPath.replace(/'/g, "''")}'` : ""} if ($dialog.ShowDialog() -eq 'OK') { Write-Output $dialog.FileName } `.trim(); return new Promise((resolve) => { const ps = spawn( "powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", psScript], { windowsHide: true } ); let output = ""; ps.stdout.on("data", (data) => { output += data.toString("utf8"); }); ps.on("close", (code) => { let selected = output.trim(); if (selected.charCodeAt(0) === 0xfeff) { selected = selected.slice(1); } writeJson(res, 200, { path: code === 0 && selected ? selected : null }); resolve(); }); ps.on("error", () => { writeJson(res, 200, { path: null }); resolve(); }); }); } function hasUsableLlmConfig(currentConfig) { return Boolean( currentConfig && stringValue(currentConfig.apiKey) && stringValue(currentConfig.baseUrl) && stringValue(currentConfig.model) ); } function sanitizeSceneIr(sceneIr) { const value = sceneIr && typeof sceneIr === "object" ? JSON.parse(JSON.stringify(sceneIr)) : {}; value.sceneId = stringValue(value.sceneId); value.sceneIdDiagnostics = sanitizeSceneIdDiagnostics(value.sceneIdDiagnostics); value.sceneName = stringValue(value.sceneName) || "Generated Scene"; value.sceneKind = stringValue(value.sceneKind) || "report_collection"; value.workflowArchetype = stringValue(value.workflowArchetype) || "single_request_table"; value.bootstrap = sanitizeBootstrap(value.bootstrap); value.params = sanitizeParams(value.params); value.modes = sanitizeModes(value.modes); value.defaultMode = stringValue(value.defaultMode) || null; value.modeSwitchField = stringValue(value.modeSwitchField) || null; value.workflowSteps = sanitizeWorkflowSteps(value.workflowSteps); value.workflowEvidence = sanitizeWorkflowEvidence(value.workflowEvidence); value.mainRequest = sanitizeMainRequest(value.mainRequest); value.enrichmentRequests = sanitizeEnrichmentRequests(value.enrichmentRequests); value.mergePlan = sanitizeMergePlan(value.mergePlan); value.requestTemplate = ensureObject(value.requestTemplate); value.responsePath = stringValue(value.responsePath); value.normalizeRules = sanitizeNormalizeRules(value.normalizeRules); value.artifactContract = sanitizeArtifactContract(value.artifactContract); value.validationHints = sanitizeValidationHints(value.validationHints); value.evidence = sanitizeEvidence(value.evidence); value.readiness = sanitizeReadiness(value.readiness); value.apiEndpoints = sanitizeApiEndpoints(value.apiEndpoints); value.staticParams = ensureObject(value.staticParams); value.columnDefs = Array.isArray(value.columnDefs) ? value.columnDefs : []; value.confidence = clampConfidence(value.confidence); value.uncertainties = sanitizeStringList(value.uncertainties); return value; } function sanitizeMainRequest(mainRequest) { const value = ensureObject(mainRequest); if (!Object.keys(value).length) return null; return { apiEndpoint: sanitizeApiEndpoint(value.apiEndpoint), requestTemplate: ensureObject(value.requestTemplate), responsePath: stringValue(value.responsePath), columnDefs: Array.isArray(value.columnDefs) ? value.columnDefs : [], }; } function sanitizeEnrichmentRequests(enrichmentRequests) { if (!Array.isArray(enrichmentRequests)) return []; return enrichmentRequests .map((item) => ({ name: stringValue(item?.name), apiEndpoint: sanitizeApiEndpoint(item?.apiEndpoint), paramBindings: ensureObject(item?.paramBindings), responsePath: stringValue(item?.responsePath), consumedFields: sanitizeStringList(item?.consumedFields), })) .filter((item) => item.name); } function sanitizeMergePlan(mergePlan) { const value = ensureObject(mergePlan); if (!Object.keys(value).length) return null; return { joinKeys: sanitizeStringList(value.joinKeys), fieldMappings: Array.isArray(value.fieldMappings) ? value.fieldMappings.map((item) => ({ outputField: stringValue(item?.outputField), sourceType: stringValue(item?.sourceType), sourceField: stringValue(item?.sourceField), requestName: stringValue(item?.requestName) || null, })).filter((item) => item.outputField) : [], aggregateRules: sanitizeStringList(value.aggregateRules), outputColumns: Array.isArray(value.outputColumns) ? value.outputColumns : [], }; } function sanitizeSceneIdDiagnostics(sceneIdDiagnostics) { const value = ensureObject(sceneIdDiagnostics); return { candidateSource: stringValue(value.candidateSource), valid: value.valid !== false, invalidReason: stringValue(value.invalidReason) || null, candidates: Array.isArray(value.candidates) ? value.candidates .map((candidate) => ({ value: stringValue(candidate?.value), source: stringValue(candidate?.source), valid: candidate?.valid !== false, reason: stringValue(candidate?.reason) || null, })) .filter((candidate) => candidate.value) : [], }; } function sanitizeBootstrap(bootstrap) { const value = ensureObject(bootstrap); return { expectedDomain: stringValue(value.expectedDomain), targetUrl: stringValue(value.targetUrl), requiresTargetPage: value.requiresTargetPage !== false, pageTitleKeywords: sanitizeStringList(value.pageTitleKeywords), source: stringValue(value.source), }; } function sanitizeParams(params) { if (!Array.isArray(params)) return []; return params .map((param) => ({ name: stringValue(param?.name), resolver: stringValue(param?.resolver), required: Boolean(param?.required), promptMissing: stringValue(param?.promptMissing), promptAmbiguous: stringValue(param?.promptAmbiguous), resolverConfig: ensureObject(param?.resolverConfig), })) .filter((param) => param.name); } function sanitizeModes(modes) { if (!Array.isArray(modes)) return []; return modes .map((mode) => ({ name: stringValue(mode?.name), label: stringValue(mode?.label) || null, condition: mode?.condition && typeof mode.condition === "object" ? { field: stringValue(mode.condition.field), operator: stringValue(mode.condition.operator) || "equals", value: mode.condition.value, } : null, apiEndpoint: sanitizeApiEndpoint(mode?.apiEndpoint), columnDefs: Array.isArray(mode?.columnDefs) ? mode.columnDefs : [], requestTemplate: ensureObject(mode?.requestTemplate), normalizeRules: sanitizeNormalizeRules(mode?.normalizeRules), responsePath: stringValue(mode?.responsePath), })) .filter((mode) => mode.name); } function sanitizeWorkflowSteps(steps) { if (!Array.isArray(steps)) return []; return steps .map((step) => ({ type: stringValue(step?.type), entry: stringValue(step?.entry) || null, source: stringValue(step?.source) || null, expr: stringValue(step?.expr) || null, description: stringValue(step?.description) || null, endpoint: stringValue(step?.endpoint) || null, })) .filter((step) => step.type); } function sanitizeWorkflowEvidence(workflowEvidence) { const value = ensureObject(workflowEvidence); return { requestEntries: sanitizeStringList(value.requestEntries), paginationFields: sanitizeStringList(value.paginationFields), secondaryRequestEntries: sanitizeStringList(value.secondaryRequestEntries), postProcessSteps: sanitizeStringList(value.postProcessSteps), }; } function sanitizeNormalizeRules(rules) { const value = ensureObject(rules); if (!Object.keys(value).length) return null; return { type: stringValue(value.type), requiredFields: sanitizeStringList(value.requiredFields), filterNull: value.filterNull !== false, }; } function sanitizeArtifactContract(contract) { const value = ensureObject(contract); return { type: stringValue(value.type) || "report-artifact", successStatus: sanitizeStringList(value.successStatus).length ? sanitizeStringList(value.successStatus) : ["ok", "partial", "empty"], failureStatus: sanitizeStringList(value.failureStatus).length ? sanitizeStringList(value.failureStatus) : ["blocked", "error"], }; } function sanitizeValidationHints(hints) { const value = ensureObject(hints); return { requiresTargetPage: value.requiresTargetPage !== false, runtimeCompatible: value.runtimeCompatible !== false, manualCompletionRequired: Boolean(value.manualCompletionRequired), missingPieces: sanitizeStringList(value.missingPieces), }; } function sanitizeEvidence(evidence) { if (!Array.isArray(evidence)) return []; return evidence .map((item) => ({ kind: stringValue(item?.kind), evidenceType: stringValue(item?.evidenceType) || "signal", layer: stringValue(item?.layer) || "business", subject: stringValue(item?.subject) || null, summary: stringValue(item?.summary), source: stringValue(item?.source) || null, confidence: clampConfidence(item?.confidence), payload: item?.payload && typeof item.payload === "object" && !Array.isArray(item.payload) ? item.payload : null, })) .filter((item) => item.summary); } function sanitizeReadiness(readiness) { const value = ensureObject(readiness); return { level: stringValue(value.level), confidence: clampConfidence(value.confidence), gates: Array.isArray(value.gates) ? value.gates .map((gate) => ({ name: stringValue(gate?.name), passed: Boolean(gate?.passed), reason: stringValue(gate?.reason) || null, })) .filter((gate) => gate.name) : [], risks: sanitizeStringList(value.risks), missingPieces: sanitizeStringList(value.missingPieces), notes: sanitizeStringList(value.notes), }; } function sanitizeApiEndpoints(endpoints) { if (!Array.isArray(endpoints)) return []; return endpoints.map(sanitizeApiEndpoint).filter(Boolean); } function sanitizeApiEndpoint(endpoint) { if (!endpoint || typeof endpoint !== "object") return null; const url = stringValue(endpoint.url); if (!url) return null; return { name: stringValue(endpoint.name) || inferEndpointName(url), url, method: stringValue(endpoint.method).toUpperCase() || "GET", contentType: stringValue(endpoint.contentType) || null, description: stringValue(endpoint.description) || null, }; } function inferEndpointName(url) { const parts = url.split(/[/?#]/).filter(Boolean); return parts[parts.length - 1] || "endpoint"; } function mergeSceneIr(deterministic, llmSceneIr, warnings) { const merged = JSON.parse(JSON.stringify(deterministic)); const llm = llmSceneIr ? sanitizeSceneIr(llmSceneIr) : null; if (!llm) { merged.sceneIdDiagnostics = sanitizeSceneIdDiagnostics(merged.sceneIdDiagnostics); merged.validationHints.manualCompletionRequired = merged.readiness.level !== "A"; return sanitizeSceneIr(merged); } const sceneIdentity = chooseSceneIdentity(deterministic, llm, warnings); merged.sceneId = sceneIdentity.sceneId; merged.sceneIdDiagnostics = sceneIdentity.sceneIdDiagnostics; merged.sceneName = chooseSoftValue(deterministic.sceneName, llm.sceneName); merged.sceneKind = chooseSoftValue(deterministic.sceneKind, llm.sceneKind) || "report_collection"; merged.workflowArchetype = chooseArchetype(deterministic, llm, warnings); merged.bootstrap = mergeBootstrap(deterministic.bootstrap, llm.bootstrap, warnings); merged.params = mergeByName(deterministic.params, llm.params); merged.modes = deterministic.modes.length ? deterministic.modes : llm.modes; merged.defaultMode = chooseSoftValue(deterministic.defaultMode, llm.defaultMode); merged.modeSwitchField = chooseSoftValue(deterministic.modeSwitchField, llm.modeSwitchField); merged.workflowSteps = mergeWorkflowSteps(deterministic.workflowSteps, llm.workflowSteps); merged.workflowEvidence = mergeWorkflowEvidence(deterministic.workflowEvidence, llm.workflowEvidence); merged.mainRequest = deterministic.mainRequest || llm.mainRequest || null; merged.enrichmentRequests = deterministic.enrichmentRequests?.length ? deterministic.enrichmentRequests : llm.enrichmentRequests; merged.mergePlan = deterministic.mergePlan || llm.mergePlan || null; merged.requestTemplate = mergeObjects(deterministic.requestTemplate, llm.requestTemplate); merged.responsePath = chooseHardValue(deterministic.responsePath, llm.responsePath); merged.normalizeRules = deterministic.normalizeRules || llm.normalizeRules || null; merged.artifactContract = deterministic.artifactContract || llm.artifactContract; merged.apiEndpoints = mergeEndpoints(deterministic.apiEndpoints, llm.apiEndpoints); merged.staticParams = mergeObjects(deterministic.staticParams, llm.staticParams); merged.columnDefs = deterministic.columnDefs.length ? deterministic.columnDefs : llm.columnDefs; merged.evidence = mergeEvidence(deterministic.evidence, llm.evidence); merged.uncertainties = uniqueStrings([...(deterministic.uncertainties || []), ...(llm.uncertainties || [])]); merged.confidence = Number( ( clampConfidence(deterministic.confidence) * 0.7 + clampConfidence(llm.confidence) * 0.3 + (llm.evidence.length ? 0.02 : 0) ).toFixed(2) ); merged.readiness = computeReadinessPreview(merged, warnings, llm.readiness); merged.validationHints = { requiresTargetPage: merged.bootstrap.requiresTargetPage !== false, runtimeCompatible: merged.params.every((param) => ["dictionary_entity", "month_week_period", "fixed_enum", "literal_passthrough"].includes(param.resolver) ), manualCompletionRequired: merged.readiness.level !== "A", missingPieces: merged.readiness.missingPieces.slice(), }; return sanitizeSceneIr(merged); } function chooseSoftValue(primary, secondary) { return stringValue(secondary) || stringValue(primary) || ""; } function chooseHardValue(primary, secondary) { const primaryValue = stringValue(primary); if (primaryValue) return primaryValue; return stringValue(secondary); } function chooseArchetype(deterministic, llm, warnings) { const deterministicValue = stringValue(deterministic.workflowArchetype); const llmValue = stringValue(llm.workflowArchetype); if (deterministicValue && llmValue && deterministicValue !== llmValue) { warnings.push(`Workflow archetype conflict: deterministic=${deterministicValue}, llm=${llmValue}`); } return deterministicValue || llmValue || "single_request_table"; } function chooseSceneIdentity(deterministic, llm, warnings) { const deterministicDiagnostics = sanitizeSceneIdDiagnostics(deterministic.sceneIdDiagnostics); const llmDiagnostics = sanitizeSceneIdDiagnostics(llm.sceneIdDiagnostics); const candidates = [ { sceneId: stringValue(llm.sceneId), diagnostics: { ...llmDiagnostics, candidateSource: llmDiagnostics.candidateSource || "llm_semantic", }, }, { sceneId: stringValue(deterministic.sceneId), diagnostics: { ...deterministicDiagnostics, candidateSource: deterministicDiagnostics.candidateSource || "deterministic", }, }, ].filter((item) => item.sceneId); const validCandidate = candidates.find((item) => item.diagnostics.valid); if ( stringValue(deterministic.sceneId) && stringValue(llm.sceneId) && stringValue(deterministic.sceneId) !== stringValue(llm.sceneId) ) { warnings.push(`SceneId conflict: deterministic=${deterministic.sceneId}, llm=${llm.sceneId}`); } if (validCandidate) { return { sceneId: validCandidate.sceneId, sceneIdDiagnostics: validCandidate.diagnostics, }; } const fallback = candidates[0] || { sceneId: "", diagnostics: { candidateSource: "", valid: false, invalidReason: "empty_scene_id", candidates: [], }, }; return { sceneId: fallback.sceneId, sceneIdDiagnostics: { ...fallback.diagnostics, valid: false, invalidReason: fallback.diagnostics.invalidReason || "invalid_scene_id", }, }; } function mergeBootstrap(deterministic, llm, warnings) { const deterministicTarget = isUnsafeBootstrapValue(deterministic.targetUrl) ? "" : stringValue(deterministic.targetUrl); const deterministicDomain = isUnsafeBootstrapValue(deterministic.expectedDomain) ? "" : stringValue(deterministic.expectedDomain); const llmTarget = isUnsafeBootstrapValue(llm.targetUrl) ? "" : stringValue(llm.targetUrl); const llmDomain = isUnsafeBootstrapValue(llm.expectedDomain) ? "" : stringValue(llm.expectedDomain); const merged = { expectedDomain: chooseHardValue(deterministicDomain, llmDomain), targetUrl: chooseHardValue(deterministicTarget, llmTarget), requiresTargetPage: deterministic.requiresTargetPage !== false && llm.requiresTargetPage !== false, pageTitleKeywords: uniqueStrings([...(deterministic.pageTitleKeywords || []), ...(llm.pageTitleKeywords || [])]), source: stringValue(deterministic.source) || stringValue(llm.source) || "deterministic", }; if (stringValue(llm.targetUrl) && !llmTarget) { warnings.push(`Ignored unsafe llm bootstrap target: ${llm.targetUrl}`); } if (stringValue(llm.expectedDomain) && !llmDomain) { warnings.push(`Ignored unsafe llm bootstrap domain: ${llm.expectedDomain}`); } if ( deterministic.targetUrl && llm.targetUrl && stringValue(deterministic.targetUrl) !== stringValue(llm.targetUrl) ) { warnings.push(`Bootstrap target conflict: deterministic=${deterministic.targetUrl}, llm=${llm.targetUrl}`); } if ( deterministic.expectedDomain && llm.expectedDomain && stringValue(deterministic.expectedDomain) !== stringValue(llm.expectedDomain) ) { warnings.push( `Bootstrap domain conflict: deterministic=${deterministic.expectedDomain}, llm=${llm.expectedDomain}` ); } return merged; } function isUnsafeBootstrapValue(value) { const text = stringValue(value).toLowerCase(); if (!text) return false; return ( text.includes("localhost") || text.includes("127.0.0.1") || text.includes("surfaceservices") || text.includes("reportservices") ); } function mergeByName(primary, secondary) { const map = new Map(); for (const item of secondary || []) { map.set(item.name, item); } for (const item of primary || []) { map.set(item.name, item); } return Array.from(map.values()).filter((item) => item && item.name); } function mergeWorkflowSteps(primary, secondary) { const merged = []; const seen = new Set(); for (const step of [...(primary || []), ...(secondary || [])]) { if (!step || !step.type) continue; const key = [step.type, step.entry || "", step.endpoint || "", step.expr || ""].join("|"); if (seen.has(key)) continue; seen.add(key); merged.push(step); } return merged; } function mergeWorkflowEvidence(primary, secondary) { return { requestEntries: uniqueStrings([...(primary?.requestEntries || []), ...(secondary?.requestEntries || [])]), paginationFields: uniqueStrings([...(primary?.paginationFields || []), ...(secondary?.paginationFields || [])]), secondaryRequestEntries: uniqueStrings([ ...(primary?.secondaryRequestEntries || []), ...(secondary?.secondaryRequestEntries || []), ]), postProcessSteps: uniqueStrings([...(primary?.postProcessSteps || []), ...(secondary?.postProcessSteps || [])]), }; } function mergeObjects(primary, secondary) { return { ...ensureObject(secondary), ...ensureObject(primary), }; } function mergeEndpoints(primary, secondary) { const map = new Map(); for (const endpoint of secondary || []) { const key = `${endpoint.method}|${endpoint.url}`; map.set(key, endpoint); } for (const endpoint of primary || []) { const key = `${endpoint.method}|${endpoint.url}`; map.set(key, endpoint); } return Array.from(map.values()); } function mergeEvidence(primary, secondary) { const seen = new Set(); const merged = []; for (const item of [...(primary || []), ...(secondary || [])]) { if (!item || !item.summary) continue; const key = `${item.kind}|${item.evidenceType || ""}|${item.summary}`; if (seen.has(key)) continue; seen.add(key); merged.push(item); } return merged; } function computeReadinessPreview(sceneIr, warnings, llmReadiness) { const risks = []; const missingPieces = []; const notes = []; const gates = []; if (!sceneIr.sceneIdDiagnostics?.valid) { missingPieces.push("invalid_scene_id"); risks.push( `Scene id is invalid${sceneIr.sceneIdDiagnostics?.invalidReason ? `: ${sceneIr.sceneIdDiagnostics.invalidReason}` : "."}` ); } const hasUnsafeBootstrap = isUnsafeBootstrapValue(sceneIr.bootstrap.targetUrl) || isUnsafeBootstrapValue(sceneIr.bootstrap.expectedDomain); if (hasUnsafeBootstrap) { missingPieces.push("bootstrap_target"); risks.push("Bootstrap resolves to localhost/helper/export instead of a business domain."); } else if (!sceneIr.bootstrap.targetUrl && !sceneIr.bootstrap.expectedDomain) { missingPieces.push("bootstrap_target"); risks.push("Bootstrap target/domain is missing."); } else if (!sceneIr.bootstrap.expectedDomain) { risks.push("Expected domain is missing."); } if (!sceneIr.apiEndpoints.length) { missingPieces.push("api_endpoint"); risks.push("No API endpoint is available."); } if (!sceneIr.workflowSteps.length) { missingPieces.push("workflow_steps"); risks.push("Workflow steps are incomplete."); } const businessEndpoints = (sceneIr.apiEndpoints || []).filter((endpoint) => !isUnsafeBootstrapValue(endpoint.url)); const requestContract = previewRequestContract(sceneIr, businessEndpoints); if (!requestContract.passed) { missingPieces.push(requestContract.reason || "request_contract"); risks.push(requestContract.message); } const responseContract = previewResponseContract(sceneIr, businessEndpoints); if (!responseContract.passed) { missingPieces.push(responseContract.reason || "response_contract"); risks.push(responseContract.message); } const workflowContract = previewWorkflowContract(sceneIr, businessEndpoints); if (!workflowContract.passed) { missingPieces.push(workflowContract.reason || "workflow_contract"); risks.push(workflowContract.message); } if (sceneIr.workflowArchetype === "multi_mode_request" && !sceneIr.modes.length) { missingPieces.push("modes"); risks.push("Multi-mode workflow has no resolved modes."); } if (sceneIr.workflowArchetype === "paginated_enrichment") { const hasPaginate = sceneIr.workflowSteps.some((step) => step.type === "paginate") || (sceneIr.workflowEvidence?.paginationFields || []).length > 0; const hasSecondary = sceneIr.workflowSteps.some((step) => step.type === "secondary_request") || (sceneIr.workflowEvidence?.secondaryRequestEntries || []).length > 0; const hasPostProcess = sceneIr.workflowSteps.some((step) => ["filter", "transform", "export"].includes(step.type)) || (sceneIr.workflowEvidence?.postProcessSteps || []).length > 0; if (!hasPaginate) { missingPieces.push("paginate_step"); risks.push("Paginated enrichment is missing a pagination step."); } if (!hasSecondary || sceneIr.apiEndpoints.length < 2) { missingPieces.push("secondary_request"); risks.push("Paginated enrichment is missing a strong secondary request path."); } if (!hasPostProcess) { missingPieces.push("post_process"); risks.push("Paginated enrichment is missing filter/transform/export evidence."); } } if (!sceneIr.validationHints.runtimeCompatible) { risks.push("Some params require runtime support not confirmed by the frontend preview."); } gates.push({ name: "scene_id_valid", passed: sceneIr.sceneIdDiagnostics?.valid !== false, reason: sceneIr.sceneIdDiagnostics?.valid === false ? sceneIr.sceneIdDiagnostics.invalidReason || "invalid_scene_id" : null, }); gates.push({ name: "bootstrap_resolved", passed: !hasUnsafeBootstrap && Boolean(sceneIr.bootstrap.targetUrl || sceneIr.bootstrap.expectedDomain), reason: !hasUnsafeBootstrap && Boolean(sceneIr.bootstrap.targetUrl || sceneIr.bootstrap.expectedDomain) ? null : "bootstrap_target", }); gates.push({ name: "request_contract_complete", passed: requestContract.passed, reason: requestContract.passed ? null : requestContract.reason, }); gates.push({ name: "response_contract_complete", passed: responseContract.passed, reason: responseContract.passed ? null : responseContract.reason, }); gates.push({ name: "workflow_contract_complete", passed: workflowContract.passed, reason: workflowContract.passed ? null : workflowContract.reason, }); gates.push({ name: "workflow_complete_for_archetype", passed: workflowContract.passed, reason: workflowContract.passed ? null : workflowContract.reason, }); gates.push({ name: "runtime_contract_compatible", passed: sceneIr.validationHints.runtimeCompatible !== false, reason: sceneIr.validationHints.runtimeCompatible !== false ? null : "runtime_contract_incompatible", }); for (const warning of warnings || []) { risks.push(warning); } if (llmReadiness && Array.isArray(llmReadiness.notes)) { notes.push(...sanitizeStringList(llmReadiness.notes)); } let level = "A"; if (missingPieces.length > 0) { level = missingPieces.length >= 2 ? "C" : "B"; } else if (risks.length > 1 || clampConfidence(sceneIr.confidence) < 0.7) { level = "B"; } if (level === "A") { notes.unshift("Ready for direct internal-network trial."); } else if (level === "B") { notes.unshift("Structurally plausible, but human review is recommended."); } else { notes.unshift("Draft only; manual completion is required before trial."); } return { level, confidence: clampConfidence( llmReadiness?.confidence ? (sceneIr.confidence * 0.7 + llmReadiness.confidence * 0.3) : sceneIr.confidence ), gates, risks: uniqueStrings(risks), missingPieces: uniqueStrings(missingPieces), notes: uniqueStrings(notes), }; } function previewRequestContract(sceneIr, businessEndpoints) { const endpointCount = (businessEndpoints || []).length; const hasRequestStep = (sceneIr.workflowSteps || []).some((step) => ["request", "paginate", "secondary_request", "page_state"].includes(step.type)); const hasRequestEvidence = (sceneIr.workflowEvidence?.requestEntries || []).length > 0; const hasParams = (sceneIr.params || []).length > 0; if (sceneIr.workflowArchetype === "single_request_enrichment") { return endpointCount >= 2 && Boolean(sceneIr.mainRequest) && (sceneIr.enrichmentRequests || []).length > 0 ? { passed: true } : { passed: false, reason: endpointCount >= 2 ? "main_request" : "request_endpoint", message: endpointCount >= 2 ? "G1-E workflow is missing main/enrichment request evidence." : "G1-E workflow requires both main and enrichment business endpoints.", }; } if (sceneIr.workflowArchetype === "multi_mode_request") { const hasModes = (sceneIr.modes || []).length > 0; const hasModeSwitch = Boolean(sceneIr.modeSwitchField); return endpointCount > 0 && hasModes && hasModeSwitch ? { passed: true } : { passed: false, reason: !hasModes || !hasModeSwitch ? "request_mode_param" : "request_endpoint", message: !hasModes || !hasModeSwitch ? "Multi-mode request is missing mode selection contract." : "Request contract is missing business endpoint evidence.", }; } if (sceneIr.workflowArchetype === "paginated_enrichment") { return endpointCount >= 2 && (hasRequestStep || hasRequestEvidence) ? { passed: true } : { passed: false, reason: endpointCount >= 2 ? "request_workflow" : "request_endpoint", message: endpointCount >= 2 ? "Paginated enrichment is missing request workflow evidence." : "Paginated enrichment requires both primary and secondary business endpoints.", }; } if (sceneIr.workflowArchetype === "page_state_eval") { return hasRequestStep || endpointCount > 0 ? { passed: true } : { passed: false, reason: "request_workflow", message: "Page-state evaluation is missing request/state workflow evidence.", }; } return endpointCount > 0 || hasRequestStep || hasRequestEvidence || hasParams ? { passed: true } : { passed: false, reason: "request_endpoint", message: "Request contract is missing business endpoint or request entry evidence.", }; } function previewResponseContract(sceneIr, businessEndpoints) { const endpointCount = (businessEndpoints || []).length; const hasTransform = (sceneIr.workflowSteps || []).some((step) => ["transform", "filter", "export"].includes(step.type)); const hasResponsePath = Boolean(sceneIr.responsePath) || (sceneIr.modes || []).some((mode) => Boolean(mode.responsePath)); if (sceneIr.workflowArchetype === "single_request_enrichment") { const hasColumns = (sceneIr.mergePlan?.outputColumns || []).length > 0 || (sceneIr.columnDefs || []).length > 0; return endpointCount >= 2 && Boolean(sceneIr.mergePlan) && hasColumns ? { passed: true } : { passed: false, reason: endpointCount >= 2 ? "merge_plan" : "response_path", message: endpointCount >= 2 ? "G1-E workflow is missing merge/output evidence." : "G1-E workflow lacks enough response-side endpoint evidence.", }; } if (sceneIr.workflowArchetype === "page_state_eval") { return { passed: true }; } if (sceneIr.workflowArchetype === "paginated_enrichment") { return endpointCount >= 2 && hasResponsePath ? { passed: true } : { passed: false, reason: !hasResponsePath ? "response_path" : "response_endpoint", message: !hasResponsePath ? "Paginated enrichment is missing response extraction path." : "Paginated enrichment lacks enough response-side endpoints.", }; } return hasResponsePath || hasTransform || endpointCount > 0 ? { passed: true } : { passed: false, reason: "response_path", message: "Response contract is missing response extraction evidence.", }; } function previewWorkflowContract(sceneIr, businessEndpoints) { const steps = sceneIr.workflowSteps || []; if (!steps.length) { return { passed: false, reason: "workflow_steps", message: "Workflow contract is missing executable steps.", }; } if (sceneIr.workflowArchetype === "single_request_enrichment") { const hasRequest = steps.some((step) => step.type === "request"); const hasEnrichment = steps.some((step) => step.type === "enrichment_request"); const hasTransform = steps.some((step) => step.type === "transform"); return hasRequest && hasEnrichment && hasTransform && Boolean(sceneIr.mergePlan) ? { passed: true } : { passed: false, reason: !hasRequest ? "workflow_request" : !hasEnrichment ? "enrichment_requests" : !hasTransform ? "workflow_transform" : "merge_plan", message: "G1-E workflow requires request, enrichment_request, transform, and merge_plan.", }; } if (sceneIr.workflowArchetype === "multi_mode_request") { const hasRequest = steps.some((step) => step.type === "request"); const hasTransform = steps.some((step) => step.type === "transform"); return hasRequest && hasTransform ? { passed: true } : { passed: false, reason: !hasRequest ? "workflow_request" : "workflow_transform", message: "Multi-mode request requires request and transform steps.", }; } if (sceneIr.workflowArchetype === "paginated_enrichment") { const hasPaginate = steps.some((step) => step.type === "paginate") || (sceneIr.workflowEvidence?.paginationFields || []).length > 0; const hasSecondary = steps.some((step) => step.type === "secondary_request") || (sceneIr.workflowEvidence?.secondaryRequestEntries || []).length > 0; const hasPostProcess = steps.some((step) => ["filter", "transform", "export"].includes(step.type)) || (sceneIr.workflowEvidence?.postProcessSteps || []).length > 0; if (!hasPaginate) { return { passed: false, reason: "paginate_step", message: "Paginated enrichment is missing a pagination step.", }; } if (!hasSecondary || (businessEndpoints || []).length < 2) { return { passed: false, reason: "secondary_request", message: "Paginated enrichment is missing a strong secondary request path.", }; } if (!hasPostProcess) { return { passed: false, reason: "post_process", message: "Paginated enrichment is missing filter/transform/export evidence.", }; } } return { passed: true }; } function getGenerationBlockers({ sceneIr, sceneId, sceneName, sourceDir }) { const blockers = []; const validation = validateSceneIdCandidate(sceneId, { sceneName, sourceDir }); if (!validation.valid) { blockers.push(`invalid_scene_id:${validation.reason}`); } if (sceneIr?.sceneIdDiagnostics && sceneIr.sceneIdDiagnostics.valid === false) { blockers.push( `analysis_invalid_scene_id:${sceneIr.sceneIdDiagnostics.invalidReason || "invalid_scene_id"}` ); } for (const gate of sceneIr?.readiness?.gates || []) { if (!gate.passed) { blockers.push(`gate_failed:${gate.name}${gate.reason ? `:${gate.reason}` : ""}`); } } return uniqueStrings(blockers); } function ensureObject(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } function sanitizeStringList(value) { if (!Array.isArray(value)) return []; return uniqueStrings(value.map((item) => stringValue(item)).filter(Boolean)); } function uniqueStrings(list) { return Array.from(new Set((list || []).map((item) => stringValue(item)).filter(Boolean))); } function clampConfidence(value) { const numeric = typeof value === "number" ? value : Number(value); if (!Number.isFinite(numeric)) return 0; return Math.max(0, Math.min(1, Number(numeric.toFixed(2)))); } function normalizeInputPath(value) { const normalized = stringValue(value); return normalized ? normalized.replace(/\\/g, "/") : ""; } function normalizeJsonInput(value) { if (!value) return null; if (typeof value === "string") return value; if (typeof value === "object") return JSON.stringify(value); return null; } function stringValue(value) { return typeof value === "string" ? value.trim() : ""; } function createServer() { return http.createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); const pathname = url.pathname; if (req.method === "OPTIONS") { res.writeHead(204, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", }); res.end(); return; } try { if (pathname === "/health" && req.method === "GET") { handleHealth(req, res); return; } if (pathname === "/analyze" && req.method === "POST") { await handleAnalyze(req, res); return; } if (pathname === "/analyze-deep" && req.method === "POST") { await handleAnalyzeDeep(req, res); return; } if (pathname === "/generate" && req.method === "POST") { await handleGenerate(req, res); return; } if (pathname === "/select-folder" && req.method === "POST") { await handleSelectFolder(req, res); return; } if (pathname === "/select-file" && req.method === "POST") { await handleSelectFile(req, res); return; } if (pathname === "/" || pathname === "/index.html") { serveStatic(res, path.join(__dirname, "sg_scene_generator.html")); return; } const filePath = path.resolve(__dirname, "." + pathname); const baseDir = path.resolve(__dirname); if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) { writeJson(res, 403, { error: "Forbidden" }); return; } if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { serveStatic(res, filePath); return; } writeJson(res, 404, { error: "Not found" }); } catch (error) { console.error(`[error] ${req.method} ${pathname}: ${error.message}`); if (!res.headersSent) { writeJson(res, 500, { error: error.message }); return; } res.end(JSON.stringify({ error: error.message })); } }); } if (require.main === module) { const server = createServer(); server.listen(PORT, HOST, () => { console.log(""); console.log(" ================================================"); console.log(" sgClaw Scene Skill Generator"); console.log(` http://${HOST}:${PORT}/`); console.log(" Press Ctrl+C to stop"); console.log(" ================================================"); console.log(""); }); process.on("SIGINT", () => { if (server.closing) return; server.closing = true; console.log("\n[info] Shutting down..."); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 2000); }); } module.exports = { computeReadinessPreview, createServer, getGenerationBlockers, mergeSceneIr, sanitizeSceneIr, sanitizeSceneIdDiagnostics, };