Upgrade request analysis routing
This commit is contained in:
parent
1cd446bae8
commit
db3e080640
19 changed files with 1520 additions and 66 deletions
|
|
@ -18,12 +18,18 @@ The goal is to give request-processing tools stable system summaries instead of
|
|||
- JSON schema for structured request parsing output.
|
||||
- `tags.json`
|
||||
- Standardized request tags for queue analysis and admin review.
|
||||
- `terminology.json`
|
||||
- Canonical editor terminology and user-language aliases.
|
||||
- `modules.json`
|
||||
- Machine-readable index of granular KB slices for targeted retrieval.
|
||||
- `queue-workflow.md`
|
||||
- First-pass automation and review flow.
|
||||
- `editor-capabilities.md`
|
||||
- Shorthand capability matrix for common request-analysis questions.
|
||||
- `systems/*.md`
|
||||
- Human-readable system notes, organized by editor subsystem.
|
||||
- `modules/*.md`
|
||||
- Smaller topical docs that can be mixed into model context without sending the whole KB.
|
||||
|
||||
## Current System Coverage
|
||||
|
||||
|
|
@ -40,14 +46,15 @@ The scaffold currently covers:
|
|||
- Persistence and save pipeline
|
||||
- Floating windows and popout shell
|
||||
- Content catalog and record APIs
|
||||
- Routing terminology and modular retrieval slices
|
||||
|
||||
## How To Use This For Request Analysis
|
||||
|
||||
Suggested queue flow:
|
||||
|
||||
1. Pull a raw request submission from the launcher request store.
|
||||
2. Retrieve likely systems from `systems.json` using tags, aliases, and file names.
|
||||
3. Provide the matching `systems/*.md` files to the local model as context.
|
||||
2. Run a routing pass using `systems.json`, `modules.json`, and `terminology.json`.
|
||||
3. Retrieve only the most relevant `systems/*.md` and `modules/*.md` files.
|
||||
4. Require the model to use only tags from `tags.json`.
|
||||
5. Ask the model to split the submission into atomic requests.
|
||||
6. Require output that matches `request-analysis-schema.json`.
|
||||
|
|
@ -56,6 +63,7 @@ Suggested queue flow:
|
|||
|
||||
## Recommended Next Additions
|
||||
|
||||
- Keep expanding modules until every major tool and runtime handoff has at least one focused doc.
|
||||
- Add concrete file-level notes for major controllers as they evolve.
|
||||
- Add a small extractor script that refreshes endpoint lists from `server.js`.
|
||||
- Add examples of well-tagged historical requests for few-shot prompting.
|
||||
|
|
|
|||
96
docs/kb/modules.json
Normal file
96
docs/kb/modules.json
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"generatedFromRepoDate": "2026-06-27",
|
||||
"modules": [
|
||||
{
|
||||
"id": "request-routing-playbook",
|
||||
"name": "Request Routing Playbook",
|
||||
"docPath": "docs/kb/modules/request-routing-playbook.md",
|
||||
"tags": ["Request Board", "Wiki", "General"],
|
||||
"systemIds": ["request-board", "launcher-home"],
|
||||
"aliases": ["triage", "routing", "request parsing", "review flow"],
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "terminology-engine-language",
|
||||
"name": "Terminology And Engine Language",
|
||||
"docPath": "docs/kb/modules/terminology-engine-language.md",
|
||||
"tags": ["General", "Wiki", "Content", "Graphics Painter", "Tiling", "Rendering", "Worlds"],
|
||||
"systemIds": ["graphics-painter", "layers-tile-editing", "rendering-viewport", "content-catalog-records", "world-bootstrap"],
|
||||
"aliases": ["synonyms", "naming", "engine terminology", "language map"],
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "launcher-and-site",
|
||||
"name": "Launcher And Website Surface",
|
||||
"docPath": "docs/kb/modules/launcher-and-site.md",
|
||||
"tags": ["Launcher", "Website", "Polish", "Request Board"],
|
||||
"systemIds": ["launcher-home", "request-board"],
|
||||
"aliases": ["site", "homepage", "launcher", "main page"],
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "request-board-operations",
|
||||
"name": "Request Board Operations",
|
||||
"docPath": "docs/kb/modules/request-board-operations.md",
|
||||
"tags": ["Request Board", "Website", "UI / Workflow", "Wiki"],
|
||||
"systemIds": ["request-board"],
|
||||
"aliases": ["request board", "admin review", "queue worker", "moderation"],
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "tile-canvas-tools",
|
||||
"name": "Tile Canvas Tools",
|
||||
"docPath": "docs/kb/modules/tile-canvas-tools.md",
|
||||
"tags": ["Tiling", "Layers", "Chunks", "UI / Workflow"],
|
||||
"systemIds": ["layers-tile-editing", "chunk-storage-streaming", "content-catalog-records"],
|
||||
"aliases": ["map painting", "rectangle tool", "brush", "eraser", "grid editing"],
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "graphics-painter-features",
|
||||
"name": "Graphics Painter Features",
|
||||
"docPath": "docs/kb/modules/graphics-painter-features.md",
|
||||
"tags": ["Graphics Painter", "Animation", "Content", "Windows"],
|
||||
"systemIds": ["graphics-painter", "content-catalog-records", "floating-window-shell"],
|
||||
"aliases": ["sprite editor", "art editor", "painting tool", "recoloring", "frame editing"],
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "chunk-and-world-ops",
|
||||
"name": "Chunk And World Operations",
|
||||
"docPath": "docs/kb/modules/chunk-and-world-ops.md",
|
||||
"tags": ["Chunks", "World Overview", "Worlds", "Performance"],
|
||||
"systemIds": ["chunk-storage-streaming", "world-overview", "world-bootstrap"],
|
||||
"aliases": ["chunk ops", "world overview", "world map", "chunk movement", "streaming"],
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "rendering-runtime-bridge",
|
||||
"name": "Rendering And Runtime Bridge",
|
||||
"docPath": "docs/kb/modules/rendering-runtime-bridge.md",
|
||||
"tags": ["Rendering", "Performance", "Animation", "Worlds"],
|
||||
"systemIds": ["rendering-viewport", "world-bootstrap", "chunk-storage-streaming"],
|
||||
"aliases": ["runtime", "game engine", "renderer", "viewport", "scene draw"],
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "content-assets-and-imports",
|
||||
"name": "Content Assets And Imports",
|
||||
"docPath": "docs/kb/modules/content-assets-and-imports.md",
|
||||
"tags": ["Content", "Graphics Painter", "Tiling", "Persistence"],
|
||||
"systemIds": ["content-catalog-records", "graphics-painter", "persistence-save-pipeline"],
|
||||
"aliases": ["assets", "tile catalog", "sprite catalog", "imports", "images"],
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "persistence-history-and-api",
|
||||
"name": "Persistence, History, And API Writes",
|
||||
"docPath": "docs/kb/modules/persistence-history-and-api.md",
|
||||
"tags": ["Persistence", "Chunks", "Content", "UI / Workflow"],
|
||||
"systemIds": ["persistence-save-pipeline", "chunk-storage-streaming", "content-catalog-records"],
|
||||
"aliases": ["saving", "undo", "redo", "history", "api writes"],
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
docs/kb/modules/chunk-and-world-ops.md
Normal file
13
docs/kb/modules/chunk-and-world-ops.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Chunk And World Operations
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module handles world-scale navigation and chunk-backed operations rather than single-asset editing.
|
||||
|
||||
## Existing Capabilities
|
||||
|
||||
- neighborhood chunk loading around the current viewport
|
||||
- chunk move, duplicate, rotate, flip, and delete flows
|
||||
- world overview interactions
|
||||
- bookmark and point-of-interest navigation
|
||||
- batch save paths for chunk edits
|
||||
12
docs/kb/modules/content-assets-and-imports.md
Normal file
12
docs/kb/modules/content-assets-and-imports.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Content Assets And Imports
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module tracks how tiles, sprites, images, and related metadata are stored, imported, previewed, and connected to editor features.
|
||||
|
||||
## Existing Capabilities
|
||||
|
||||
- content records for tiles, sprites, and images
|
||||
- imports and source-image handling
|
||||
- preview generation for art assets
|
||||
- tile and sprite catalog usage across editor tools
|
||||
15
docs/kb/modules/graphics-painter-features.md
Normal file
15
docs/kb/modules/graphics-painter-features.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Graphics Painter Features
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module is the detailed quick-reference for the asset art editor. It is where requests about painting graphics, recoloring sprites, frame editing, and art-side tooling should route.
|
||||
|
||||
## Existing Capabilities
|
||||
|
||||
- rectangle, circle, triangle, and line tools
|
||||
- outline, fill, and combined shape modes
|
||||
- shape erasers
|
||||
- animation timeline editing
|
||||
- frame duplication and ordering workflows
|
||||
- preview background support
|
||||
- animation preview window
|
||||
14
docs/kb/modules/launcher-and-site.md
Normal file
14
docs/kb/modules/launcher-and-site.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Launcher And Website Surface
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module focuses on the public-facing launcher, its presentation, its navigation, and the supporting request/news board behavior shown outside the main editor runtime.
|
||||
|
||||
## Features
|
||||
|
||||
- floating-window launch flow for the studio
|
||||
- branded landing presentation
|
||||
- launcher background art treatment
|
||||
- request/news tab switching
|
||||
- repo link and launch affordances
|
||||
- admin-window entry point from the public board
|
||||
12
docs/kb/modules/persistence-history-and-api.md
Normal file
12
docs/kb/modules/persistence-history-and-api.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Persistence, History, And API Writes
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module explains how edits become saved data, how history interacts with those edits, and where API writes sit in the workflow.
|
||||
|
||||
## Existing Capabilities
|
||||
|
||||
- save pipeline for chunk data
|
||||
- save pipeline for content records
|
||||
- history-aware editor state
|
||||
- server endpoints for persisted editor data
|
||||
12
docs/kb/modules/rendering-runtime-bridge.md
Normal file
12
docs/kb/modules/rendering-runtime-bridge.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Rendering And Runtime Bridge
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module explains the handoff between authored editor data and runtime-facing map rendering behavior.
|
||||
|
||||
## What It Usually Means
|
||||
|
||||
- how edited data appears in the viewport
|
||||
- whether animations actually play in live rendering
|
||||
- how runtime entities or assets would consume editor-authored records
|
||||
- how gameplay requests may imply editor-side metadata
|
||||
15
docs/kb/modules/request-board-operations.md
Normal file
15
docs/kb/modules/request-board-operations.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Request Board Operations
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module describes the request intake, queue automation, admin review experience, deletion flow, and protected moderation tools.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- public request submission storage
|
||||
- pending versus active request state
|
||||
- structured review metadata
|
||||
- admin-protected editing and deletion
|
||||
- queue worker launch and rerun flows
|
||||
- recent log viewing
|
||||
- popup review details for long-form analysis
|
||||
39
docs/kb/modules/request-routing-playbook.md
Normal file
39
docs/kb/modules/request-routing-playbook.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Request Routing Playbook
|
||||
|
||||
## Purpose
|
||||
|
||||
This module exists to help the queue worker turn messy, broad, or slang-heavy requests into a useful first interpretation before the deeper implementation pass begins.
|
||||
|
||||
## Routing Rules
|
||||
|
||||
- Prefer existing Worldshaper terminology when a user uses nearby language.
|
||||
- Split one submission into multiple request items only when the text clearly asks for separate changes.
|
||||
- If a request is broad, still produce a useful interpretation instead of returning an empty or dismissive review.
|
||||
- Low confidence should change the recommendation and review notes, not erase the attempt to help.
|
||||
|
||||
## Ambiguous Request Handling
|
||||
|
||||
- "Make the game better" is not zero-information. Treat it as a broad improvement request and map it to `General` plus the most likely systems.
|
||||
- "Add horses" is a content-plus-runtime request unless the text narrows it down. It may imply:
|
||||
- visual assets
|
||||
- placement on maps
|
||||
- runtime entity support
|
||||
- "Fix painting" should first determine whether the user means map tile painting or the Graphics Painter.
|
||||
|
||||
## What The First Pass Should Produce
|
||||
|
||||
- matched terms and likely meanings
|
||||
- candidate tags
|
||||
- likely systems
|
||||
- ambiguity level
|
||||
- a short rationale safe for admin review
|
||||
- possible directions if the user intent could branch
|
||||
|
||||
## What The Second Pass Should Produce
|
||||
|
||||
- clean title
|
||||
- primary category
|
||||
- standardized tags
|
||||
- parsed interpretation
|
||||
- implementation approach
|
||||
- review rationale and options when the request is still uncertain
|
||||
20
docs/kb/modules/terminology-engine-language.md
Normal file
20
docs/kb/modules/terminology-engine-language.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Terminology And Engine Language
|
||||
|
||||
## Purpose
|
||||
|
||||
Users often describe systems with game-development slang instead of the editor's exact names. This module helps map that language back into Worldshaper concepts.
|
||||
|
||||
## Common Term Bridges
|
||||
|
||||
- "sprite editor", "painting tool", and "recoloring tool" usually point toward `Graphics Painter`.
|
||||
- "map painting", "brush", and "grid editing" usually point toward `Tile Canvas Tools` and `Layers And Tile Editing`.
|
||||
- "engine", "runtime", or "actual game" usually point toward runtime-facing systems, not just editor windows.
|
||||
- "characters", "actors", and "entities" often cross `Content`, `Rendering`, and `Worlds`.
|
||||
- "map", "grid", and "tiles" can refer to chunk-backed tile placement rather than image editing.
|
||||
|
||||
## Meaning Differences That Matter
|
||||
|
||||
- Painting on the map is not the same as painting an asset.
|
||||
- A tile is usually a world-placement record. A sprite is usually an art asset or entity-facing image record.
|
||||
- A chunk is the storage and streaming unit, not just a visible square on screen.
|
||||
- A runtime request can require editor metadata even if the user only talks about gameplay.
|
||||
14
docs/kb/modules/tile-canvas-tools.md
Normal file
14
docs/kb/modules/tile-canvas-tools.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Tile Canvas Tools
|
||||
|
||||
## What This Module Covers
|
||||
|
||||
This module is about painting and editing the live map grid, not editing the source art for tiles or sprites.
|
||||
|
||||
## Existing Capabilities
|
||||
|
||||
- freehand paint on the active layer
|
||||
- rectangle, circle, and line-based placement workflows
|
||||
- erasing on active layers
|
||||
- height-aware sparse paint behavior
|
||||
- background tile and background-hole modes
|
||||
- grid-backed placement within chunk data
|
||||
|
|
@ -15,13 +15,14 @@ Turn raw launcher submissions into structured request items that are:
|
|||
## Recommended Pipeline
|
||||
|
||||
1. Read one pending submission.
|
||||
2. Split it into candidate atomic requests.
|
||||
2. Run a light routing pass that maps slang and broad language onto likely systems and tags.
|
||||
3. Retrieve likely systems from `docs/kb/systems.json`.
|
||||
4. Load the matching `docs/kb/systems/*.md` files.
|
||||
5. Load the standardized request tags from `docs/kb/tags.json`.
|
||||
6. Ask the local model for JSON that matches `docs/kb/request-analysis-schema.json`.
|
||||
7. Validate the JSON.
|
||||
8. Promote high-confidence items and hold low-confidence items for review.
|
||||
4. Retrieve likely focused modules from `docs/kb/modules.json`.
|
||||
5. Load only the matching `docs/kb/systems/*.md` and `docs/kb/modules/*.md` files.
|
||||
6. Load the standardized request tags from `docs/kb/tags.json`.
|
||||
7. Ask the local model for JSON that matches `docs/kb/request-analysis-schema.json`.
|
||||
8. Validate the JSON.
|
||||
9. Promote high-confidence items and hold low-confidence items for review.
|
||||
|
||||
## Suggested Confidence Rules
|
||||
|
||||
|
|
@ -41,7 +42,9 @@ Search `systems.json` using:
|
|||
- aliases
|
||||
- past tags from similar requests
|
||||
|
||||
If a submission touches multiple subsystems, feed the model the top two to four matching docs instead of the whole KB.
|
||||
If a submission touches multiple subsystems, feed the model the top two to four matching system docs plus the smallest helpful focused modules instead of the whole KB.
|
||||
|
||||
Use `terminology.json` during the routing pass so phrases like "sprite editor", "painting tool", "recoloring", and "engine" still resolve to the intended Worldshaper concepts.
|
||||
|
||||
## Prompt Skeleton
|
||||
|
||||
|
|
@ -67,6 +70,7 @@ If you are unsure, lower confidence and use statusRecommendation = "needs_review
|
|||
Then provide:
|
||||
|
||||
- the raw submission
|
||||
- the routing summary
|
||||
- the JSON schema
|
||||
- the retrieved KB docs
|
||||
|
||||
|
|
@ -121,6 +125,7 @@ Behavior:
|
|||
- high-confidence all-active results replace the pending submission with active request rows
|
||||
- mixed or lower-confidence results stay on the pending submission as review metadata
|
||||
- failures are stored as analysis errors instead of silently disappearing
|
||||
- broad or ambiguous requests should still receive a useful interpretation, likely systems, and possible directions instead of a dead-end review
|
||||
|
||||
## What Not To Automate First
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,70 @@
|
|||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"routing": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string"
|
||||
},
|
||||
"ambiguity": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"]
|
||||
},
|
||||
"matchedTerms": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"suggestedTags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"suggestedSystems": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"suggestedModules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"rationale": {
|
||||
"type": "string"
|
||||
},
|
||||
"possibleDirections": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"kbSections": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
|
|
|
|||
66
docs/kb/terminology.json
Normal file
66
docs/kb/terminology.json
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"generatedFromRepoDate": "2026-06-27",
|
||||
"terms": [
|
||||
{
|
||||
"canonical": "Graphics Painter",
|
||||
"aliases": ["graphic painter", "sprite editor", "art editor", "painting tool", "pixel editor", "recoloring tool"],
|
||||
"tags": ["Graphics Painter", "Content", "Animation"],
|
||||
"systemIds": ["graphics-painter", "content-catalog-records"]
|
||||
},
|
||||
{
|
||||
"canonical": "Tile Canvas",
|
||||
"aliases": ["map canvas", "map paint", "tile brush", "grid painter", "rectangle tile tool", "tile tool"],
|
||||
"tags": ["Tiling", "Layers", "Chunks"],
|
||||
"systemIds": ["layers-tile-editing", "chunk-storage-streaming"]
|
||||
},
|
||||
{
|
||||
"canonical": "Chunk",
|
||||
"aliases": ["map section", "region", "chunk cell", "chunk area"],
|
||||
"tags": ["Chunks", "World Overview", "Worlds"],
|
||||
"systemIds": ["chunk-storage-streaming", "world-overview"]
|
||||
},
|
||||
{
|
||||
"canonical": "World Overview",
|
||||
"aliases": ["overview map", "chunk overview", "world map", "big map"],
|
||||
"tags": ["World Overview", "Chunks", "Worlds"],
|
||||
"systemIds": ["world-overview", "chunk-storage-streaming"]
|
||||
},
|
||||
{
|
||||
"canonical": "Renderer",
|
||||
"aliases": ["runtime renderer", "viewport", "draw system", "scene draw", "engine view"],
|
||||
"tags": ["Rendering", "Performance", "Animation"],
|
||||
"systemIds": ["rendering-viewport"]
|
||||
},
|
||||
{
|
||||
"canonical": "Runtime",
|
||||
"aliases": ["engine", "game runtime", "play mode", "actual game"],
|
||||
"tags": ["Worlds", "Rendering", "Performance"],
|
||||
"systemIds": ["world-bootstrap", "rendering-viewport"]
|
||||
},
|
||||
{
|
||||
"canonical": "Character Entity",
|
||||
"aliases": ["character", "npc", "actor", "unit", "entity"],
|
||||
"tags": ["Content", "Rendering", "Worlds"],
|
||||
"systemIds": ["content-catalog-records", "rendering-viewport", "world-bootstrap"]
|
||||
},
|
||||
{
|
||||
"canonical": "Grid",
|
||||
"aliases": ["tile grid", "map grid", "cells", "squares"],
|
||||
"tags": ["Tiling", "Chunks", "Layers"],
|
||||
"systemIds": ["layers-tile-editing", "chunk-storage-streaming"]
|
||||
},
|
||||
{
|
||||
"canonical": "Asset Catalog",
|
||||
"aliases": ["sprite list", "tile list", "catalog", "content records", "asset database"],
|
||||
"tags": ["Content", "Persistence"],
|
||||
"systemIds": ["content-catalog-records", "persistence-save-pipeline"]
|
||||
},
|
||||
{
|
||||
"canonical": "Launcher Request Board",
|
||||
"aliases": ["request board", "requests", "admin review", "pending queue"],
|
||||
"tags": ["Request Board", "Website", "Wiki"],
|
||||
"systemIds": ["request-board", "launcher-home"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
263
server.js
263
server.js
|
|
@ -101,6 +101,9 @@ const editorSettingsPath = path.join(dataRoot, "editor_settings.json");
|
|||
const launcherRequestsPath = path.join(dataRoot, "launcher_requests.json");
|
||||
const requestAnalysisWorkerScriptPath = path.join(__dirname, "scripts", "request-analysis-worker.mjs");
|
||||
const requestTagCatalogPath = path.join(__dirname, "docs", "kb", "tags.json");
|
||||
const kbSystemsIndexPath = path.join(__dirname, "docs", "kb", "systems.json");
|
||||
const kbModulesIndexPath = path.join(__dirname, "docs", "kb", "modules.json");
|
||||
const kbTerminologyPath = path.join(__dirname, "docs", "kb", "terminology.json");
|
||||
const requestAnalysisRunState = {
|
||||
child: null,
|
||||
restartTimer: null,
|
||||
|
|
@ -492,6 +495,41 @@ function normalizeLauncherRequestAnalysisTags(value, fallback = []) {
|
|||
return normalizeLauncherRequestTags(Array.isArray(fallback) ? fallback : []);
|
||||
}
|
||||
|
||||
function normalizeLauncherRequestAnalysisRouting(value) {
|
||||
const source = value && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: null;
|
||||
if (!source) {
|
||||
return undefined;
|
||||
}
|
||||
const ambiguityRaw = String(source.ambiguity || "").trim().toLowerCase();
|
||||
const ambiguity = ambiguityRaw === "low" || ambiguityRaw === "medium" || ambiguityRaw === "high"
|
||||
? ambiguityRaw
|
||||
: "medium";
|
||||
const summary = String(source.summary || source.routingSummary || "").trim();
|
||||
const rationale = String(source.rationale || "").trim();
|
||||
const matchedTerms = normalizeLauncherRequestAnalysisStringList(source.matchedTerms || source.terms);
|
||||
const suggestedTags = normalizeLauncherRequestAnalysisTags(source.suggestedTags || source.tags);
|
||||
const suggestedSystems = normalizeLauncherRequestAnalysisStringList(source.suggestedSystems || source.suggestedSystemIds);
|
||||
const suggestedModules = normalizeLauncherRequestAnalysisStringList(source.suggestedModules || source.suggestedModuleIds);
|
||||
const possibleDirections = normalizeLauncherRequestAnalysisStringList(source.possibleDirections);
|
||||
const kbSections = normalizeLauncherRequestAnalysisStringList(source.kbSections);
|
||||
if (!summary && !rationale && matchedTerms.length === 0 && suggestedTags.length === 0 && suggestedSystems.length === 0 && suggestedModules.length === 0 && possibleDirections.length === 0 && kbSections.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
summary: summary || undefined,
|
||||
ambiguity,
|
||||
matchedTerms,
|
||||
suggestedTags,
|
||||
suggestedSystems,
|
||||
suggestedModules,
|
||||
rationale: rationale || undefined,
|
||||
possibleDirections,
|
||||
kbSections,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLauncherRequestAnalysisItem(item, index = 0) {
|
||||
const source = item && typeof item === "object" && !Array.isArray(item)
|
||||
? item
|
||||
|
|
@ -567,7 +605,8 @@ function normalizeLauncherRequestAnalysis(value) {
|
|||
const updatedAt = String(source.updatedAt || "").trim() || createdAt;
|
||||
const submissionId = String(source.submissionId || "").trim();
|
||||
const sourceTextSnapshot = String(source.sourceTextSnapshot || source.sourceText || "").trim();
|
||||
if (!model && !error && !submissionId && !sourceTextSnapshot && normalizedItems.length === 0 && state === "unprocessed") {
|
||||
const routing = normalizeLauncherRequestAnalysisRouting(source.routing);
|
||||
if (!model && !error && !submissionId && !sourceTextSnapshot && normalizedItems.length === 0 && state === "unprocessed" && !routing) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
|
|
@ -579,6 +618,7 @@ function normalizeLauncherRequestAnalysis(value) {
|
|||
error: error || undefined,
|
||||
submissionId: submissionId || undefined,
|
||||
sourceTextSnapshot: sourceTextSnapshot || undefined,
|
||||
routing,
|
||||
itemCount: normalizedItems.length,
|
||||
items: normalizedItems,
|
||||
};
|
||||
|
|
@ -698,10 +738,16 @@ function isLauncherRequestAutoQueueEligible(entry) {
|
|||
&& (!analysisState || analysisState === "unprocessed");
|
||||
}
|
||||
|
||||
function getQueuedPendingLauncherRequestCount() {
|
||||
function getQueuedPendingLauncherRequestCount(requestId = "") {
|
||||
try {
|
||||
const payload = readLauncherRequestsPayload();
|
||||
return payload.requests.filter((entry) => isLauncherRequestAutoQueueEligible(entry)).length;
|
||||
const normalizedRequestId = String(requestId || "").trim();
|
||||
return payload.requests.filter((entry) => {
|
||||
if (!isLauncherRequestAutoQueueEligible(entry)) {
|
||||
return false;
|
||||
}
|
||||
return !normalizedRequestId || String(entry?.id || "").trim() === normalizedRequestId;
|
||||
}).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -734,13 +780,14 @@ function scheduleQueuedRequestAnalysis(reason = "queued-pending-requests", delay
|
|||
return true;
|
||||
}
|
||||
|
||||
function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
|
||||
function launchQueuedRequestAnalysis(reason = "queued-pending-requests", options = {}) {
|
||||
const requestId = String(options?.requestId || "").trim();
|
||||
if (!isRequestAnalysisConfigured()) {
|
||||
return { launched: false, reason: "request-analysis-not-configured" };
|
||||
}
|
||||
const queuedPendingCount = getQueuedPendingLauncherRequestCount();
|
||||
const queuedPendingCount = getQueuedPendingLauncherRequestCount(requestId);
|
||||
if (queuedPendingCount <= 0) {
|
||||
return { launched: false, reason: "no-pending-requests" };
|
||||
return { launched: false, reason: requestId ? "request-not-queued" : "no-pending-requests" };
|
||||
}
|
||||
if (requestAnalysisRunState.child && !requestAnalysisRunState.child.killed) {
|
||||
return { launched: false, reason: "request-analysis-already-running" };
|
||||
|
|
@ -755,6 +802,9 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
|
|||
"--limit",
|
||||
String(Math.max(1, Math.floor(Number(process.env.REQUEST_ANALYZER_AUTORUN_LIMIT) || 5))),
|
||||
];
|
||||
if (requestId) {
|
||||
args.push("--request-id", requestId);
|
||||
}
|
||||
const child = spawn(process.execPath, args, {
|
||||
cwd: __dirname,
|
||||
env: {
|
||||
|
|
@ -770,6 +820,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
|
|||
provider,
|
||||
reason,
|
||||
queuedPendingCount,
|
||||
requestId,
|
||||
pid: child.pid,
|
||||
});
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
|
|
@ -792,6 +843,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
|
|||
provider,
|
||||
reason,
|
||||
queuedPendingCount: getQueuedPendingLauncherRequestCount(),
|
||||
requestId,
|
||||
code: Number.isFinite(Number(code)) ? Number(code) : null,
|
||||
signal: signal || "",
|
||||
});
|
||||
|
|
@ -806,6 +858,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
|
|||
type: "launcher-request-analysis-launch-error",
|
||||
provider,
|
||||
reason,
|
||||
requestId,
|
||||
error: String(error),
|
||||
});
|
||||
});
|
||||
|
|
@ -814,6 +867,7 @@ function launchQueuedRequestAnalysis(reason = "queued-pending-requests") {
|
|||
launched: true,
|
||||
reason,
|
||||
queuedPendingCount,
|
||||
requestId,
|
||||
pid: child.pid,
|
||||
};
|
||||
}
|
||||
|
|
@ -2240,6 +2294,33 @@ function loadRequestTagCatalog() {
|
|||
return DEFAULT_REQUEST_TAG_DEFINITIONS;
|
||||
}
|
||||
|
||||
function readKbIndexFile(filePath, fallbackKey) {
|
||||
try {
|
||||
const payload = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
} catch {
|
||||
// Ignore KB read failures and fall back to empty payloads.
|
||||
}
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
[fallbackKey]: [],
|
||||
};
|
||||
}
|
||||
|
||||
function readKbSystemsIndex() {
|
||||
return readKbIndexFile(kbSystemsIndexPath, "systems");
|
||||
}
|
||||
|
||||
function readKbModulesIndex() {
|
||||
return readKbIndexFile(kbModulesIndexPath, "modules");
|
||||
}
|
||||
|
||||
function readKbTerminologyIndex() {
|
||||
return readKbIndexFile(kbTerminologyPath, "terms");
|
||||
}
|
||||
|
||||
function normalizeRequestTag(value) {
|
||||
const normalizedValue = normalizeRequestTagLookupValue(value);
|
||||
if (!normalizedValue) {
|
||||
|
|
@ -3029,6 +3110,76 @@ app.post("/api/launcher-requests/:requestId/process-analysis", (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
app.post("/api/launcher-requests/:requestId/requeue-analysis", (req, res) => {
|
||||
const requestId = String(req.params.requestId || "").trim();
|
||||
if (!requireLauncherAdminAccess(req, res)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = readLauncherRequestsPayload();
|
||||
const index = payload.requests.findIndex((entry) => entry.id === requestId);
|
||||
if (index < 0) {
|
||||
res.status(404).json({ error: "Request not found." });
|
||||
return;
|
||||
}
|
||||
const existing = payload.requests[index];
|
||||
if (existing.status !== "pending") {
|
||||
res.status(400).json({ error: "Only pending requests can be re-reviewed." });
|
||||
return;
|
||||
}
|
||||
const mode = String(req.body?.mode || "saved").trim().toLowerCase() === "draft" ? "draft" : "saved";
|
||||
const now = new Date().toISOString();
|
||||
const draftSource = mode === "draft" && req.body?.request && typeof req.body.request === "object" && !Array.isArray(req.body.request)
|
||||
? req.body.request
|
||||
: {};
|
||||
const nextRequest = normalizeLauncherRequestEntry({
|
||||
...existing,
|
||||
title: mode === "draft" ? draftSource.title ?? existing.title : existing.title,
|
||||
category: mode === "draft" ? draftSource.category ?? existing.category : existing.category,
|
||||
tags: mode === "draft" && Array.isArray(draftSource.tags) ? draftSource.tags : existing.tags,
|
||||
sourceText: mode === "draft" ? draftSource.sourceText ?? existing.sourceText : existing.sourceText,
|
||||
summary: mode === "draft" ? draftSource.summary ?? existing.summary : existing.summary,
|
||||
implementationNotes: mode === "draft" ? draftSource.implementationNotes ?? existing.implementationNotes : existing.implementationNotes,
|
||||
analysis: {
|
||||
...(existing.analysis && typeof existing.analysis === "object" && !Array.isArray(existing.analysis) ? existing.analysis : {}),
|
||||
state: "unprocessed",
|
||||
confidence: null,
|
||||
updatedAt: now,
|
||||
createdAt: existing.analysis?.createdAt || now,
|
||||
submissionId: requestId,
|
||||
sourceTextSnapshot: mode === "draft"
|
||||
? String((draftSource.sourceText ?? existing.sourceText) || "").trim()
|
||||
: String(existing.sourceText || "").trim(),
|
||||
error: "",
|
||||
routing: undefined,
|
||||
items: [],
|
||||
},
|
||||
updatedAt: now,
|
||||
}, index);
|
||||
const nextRequests = [...payload.requests];
|
||||
nextRequests[index] = nextRequest;
|
||||
writeLauncherRequestsPayload({
|
||||
schemaVersion: payload.schemaVersion,
|
||||
requests: nextRequests,
|
||||
});
|
||||
recordSaveEvent({
|
||||
type: "launcher-request-analysis-requeue",
|
||||
requestId,
|
||||
reason: mode === "draft" ? "draft-resubmitted" : "saved-request-rerun",
|
||||
textPreview: String(nextRequest.sourceText || "").slice(0, 80),
|
||||
});
|
||||
const launchResult = launchQueuedRequestAnalysis("manual-request-rerun", { requestId });
|
||||
res.json({
|
||||
ok: true,
|
||||
request: nextRequest,
|
||||
requests: nextRequests,
|
||||
...launchResult,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: `Failed to requeue launcher request analysis: ${String(err)}` });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/launcher-requests/process-pending", (req, res) => {
|
||||
if (!requireLauncherAdminAccess(req, res)) {
|
||||
return;
|
||||
|
|
@ -3047,6 +3198,106 @@ app.post("/api/launcher-requests/process-pending", (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
app.post("/api/admin/kb/query", (req, res) => {
|
||||
if (!requireLauncherAdminAccess(req, res)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const systemsIndex = readKbSystemsIndex();
|
||||
const modulesIndex = readKbModulesIndex();
|
||||
const terminologyIndex = readKbTerminologyIndex();
|
||||
const requestedTags = normalizeLauncherRequestTags(req.body?.tags || []);
|
||||
const requestedSystems = normalizeLauncherRequestAnalysisStringList(req.body?.systems || req.body?.systemIds);
|
||||
const requestedModules = normalizeLauncherRequestAnalysisStringList(req.body?.modules || req.body?.moduleIds);
|
||||
const searchTerms = normalizeLauncherRequestAnalysisStringList(req.body?.searchTerms || req.body?.terms);
|
||||
const limit = Math.max(1, Math.min(12, Math.floor(Number(req.body?.limit) || 6)));
|
||||
const searchNeedles = [
|
||||
...requestedTags,
|
||||
...requestedSystems,
|
||||
...requestedModules,
|
||||
...searchTerms,
|
||||
].map((entry) => String(entry || "").trim().toLowerCase()).filter(Boolean);
|
||||
const rankEntry = (entry, extraText = "") => {
|
||||
const corpus = [
|
||||
entry?.id,
|
||||
entry?.name,
|
||||
entry?.label,
|
||||
...(Array.isArray(entry?.aliases) ? entry.aliases : []),
|
||||
...(Array.isArray(entry?.tags) ? entry.tags : []),
|
||||
...(Array.isArray(entry?.systemIds) ? entry.systemIds : []),
|
||||
extraText,
|
||||
].join(" ").toLowerCase();
|
||||
return searchNeedles.reduce((score, needle) => (corpus.includes(needle) ? score + 2 : score), 0);
|
||||
};
|
||||
const matchedSystems = (Array.isArray(systemsIndex.systems) ? systemsIndex.systems : [])
|
||||
.map((system) => ({
|
||||
system,
|
||||
score: rankEntry(system, [system.docPath, ...(Array.isArray(system.uiSurfaces) ? system.uiSurfaces : [])].join(" ")),
|
||||
}))
|
||||
.filter(({ system, score }) => {
|
||||
if (requestedSystems.length > 0 && requestedSystems.includes(String(system.id || "").trim())) {
|
||||
return true;
|
||||
}
|
||||
if (requestedTags.length > 0 && (Array.isArray(system.tags) ? system.tags : []).some((tag) => requestedTags.includes(String(tag || "").trim()))) {
|
||||
return true;
|
||||
}
|
||||
return score > 0;
|
||||
})
|
||||
.sort((left, right) => right.score - left.score)
|
||||
.slice(0, limit)
|
||||
.map(({ system }) => ({
|
||||
...system,
|
||||
docText: fs.readFileSync(path.join(__dirname, String(system.docPath || "").replace(/\//g, path.sep)), "utf8"),
|
||||
}));
|
||||
const matchedModules = (Array.isArray(modulesIndex.modules) ? modulesIndex.modules : [])
|
||||
.map((moduleEntry) => ({
|
||||
moduleEntry,
|
||||
score: rankEntry(moduleEntry, moduleEntry.docPath),
|
||||
}))
|
||||
.filter(({ moduleEntry, score }) => {
|
||||
if (requestedModules.length > 0 && requestedModules.includes(String(moduleEntry.id || "").trim())) {
|
||||
return true;
|
||||
}
|
||||
if (requestedSystems.length > 0 && (Array.isArray(moduleEntry.systemIds) ? moduleEntry.systemIds : []).some((systemId) => requestedSystems.includes(String(systemId || "").trim()))) {
|
||||
return true;
|
||||
}
|
||||
if (requestedTags.length > 0 && (Array.isArray(moduleEntry.tags) ? moduleEntry.tags : []).some((tag) => requestedTags.includes(String(tag || "").trim()))) {
|
||||
return true;
|
||||
}
|
||||
return score > 0;
|
||||
})
|
||||
.sort((left, right) => right.score - left.score)
|
||||
.slice(0, limit)
|
||||
.map(({ moduleEntry }) => ({
|
||||
...moduleEntry,
|
||||
docText: fs.readFileSync(path.join(__dirname, String(moduleEntry.docPath || "").replace(/\//g, path.sep)), "utf8"),
|
||||
}));
|
||||
const matchedTerms = (Array.isArray(terminologyIndex.terms) ? terminologyIndex.terms : [])
|
||||
.filter((term) => {
|
||||
const corpus = [
|
||||
term?.canonical,
|
||||
...(Array.isArray(term?.aliases) ? term.aliases : []),
|
||||
...(Array.isArray(term?.tags) ? term.tags : []),
|
||||
...(Array.isArray(term?.systemIds) ? term.systemIds : []),
|
||||
].join(" ").toLowerCase();
|
||||
return searchNeedles.length === 0 || searchNeedles.some((needle) => corpus.includes(needle));
|
||||
})
|
||||
.slice(0, limit);
|
||||
res.json({
|
||||
ok: true,
|
||||
requestedTags,
|
||||
requestedSystems,
|
||||
requestedModules,
|
||||
searchTerms,
|
||||
systems: matchedSystems,
|
||||
modules: matchedModules,
|
||||
terminology: matchedTerms,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: `Failed to query KB: ${String(err)}` });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/world-default", (_req, res) => {
|
||||
try {
|
||||
const indexPayload = readWorldIndexPayload();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,19 @@ type WorldDefaultPayload = {
|
|||
|
||||
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
|
||||
type BoardTab = "news" | "requests";
|
||||
type LauncherWindowMode = "public" | "admin";
|
||||
|
||||
type LauncherRequestAnalysisRouting = {
|
||||
summary?: string;
|
||||
ambiguity?: "low" | "medium" | "high";
|
||||
matchedTerms?: string[];
|
||||
suggestedTags?: string[];
|
||||
suggestedSystems?: string[];
|
||||
suggestedModules?: string[];
|
||||
rationale?: string;
|
||||
possibleDirections?: string[];
|
||||
kbSections?: string[];
|
||||
};
|
||||
|
||||
type LauncherRequest = {
|
||||
id: string;
|
||||
|
|
@ -40,6 +53,7 @@ type LauncherRequest = {
|
|||
error?: string;
|
||||
submissionId?: string;
|
||||
sourceTextSnapshot?: string;
|
||||
routing?: LauncherRequestAnalysisRouting;
|
||||
itemCount?: number;
|
||||
items?: Array<{
|
||||
title?: string;
|
||||
|
|
@ -99,6 +113,17 @@ type ProcessPendingPayload = {
|
|||
pid?: number;
|
||||
};
|
||||
|
||||
type RequeueAnalysisPayload = {
|
||||
ok?: boolean;
|
||||
launched?: boolean;
|
||||
reason?: string;
|
||||
request?: LauncherRequest;
|
||||
requests?: LauncherRequest[];
|
||||
requestId?: string;
|
||||
queuedPendingCount?: number;
|
||||
pid?: number;
|
||||
};
|
||||
|
||||
type LauncherRequestMetaPayload = {
|
||||
allowedTags?: string[];
|
||||
};
|
||||
|
|
@ -112,6 +137,14 @@ type AdminAuthPayload = {
|
|||
|
||||
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
|
||||
|
||||
function readLauncherWindowMode(): LauncherWindowMode {
|
||||
if (typeof window === "undefined") {
|
||||
return "public";
|
||||
}
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
return searchParams.get("admin") === "requests" ? "admin" : "public";
|
||||
}
|
||||
|
||||
async function resolveDefaultWorldId(): Promise<string> {
|
||||
const response = await fetch("/api/world-default");
|
||||
if (!response.ok) {
|
||||
|
|
@ -186,6 +219,7 @@ function openReviewDetailsPopup(requestEntry: LauncherRequest): void {
|
|||
if (!popup) {
|
||||
return;
|
||||
}
|
||||
const routing = requestEntry.analysis?.routing;
|
||||
const items = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis.items : [];
|
||||
const renderedItems = items.map((item, index) => {
|
||||
const reviewOptions = Array.isArray(item?.reviewOptions) ? item.reviewOptions : [];
|
||||
|
|
@ -251,6 +285,38 @@ function openReviewDetailsPopup(requestEntry: LauncherRequest): void {
|
|||
<h1>${escapePopupHtml(requestEntry.title)}</h1>
|
||||
<p>${escapePopupHtml(requestEntry.sourceText)}</p>
|
||||
</section>
|
||||
${routing ? `
|
||||
<section class="review-card">
|
||||
<div class="review-kicker">Routing Summary</div>
|
||||
<h2>${escapePopupHtml(String(routing.summary || "KB routing context"))}</h2>
|
||||
<div class="review-meta">
|
||||
<span>Ambiguity: ${escapePopupHtml(String(routing.ambiguity || "medium"))}</span>
|
||||
${(Array.isArray(routing.suggestedTags) ? routing.suggestedTags : []).map((tag) => `<span>${escapePopupHtml(tag)}</span>`).join("")}
|
||||
</div>
|
||||
<div class="review-block">
|
||||
<h3>Rationale</h3>
|
||||
<p>${escapePopupHtml(String(routing.rationale || "No routing rationale was stored."))}</p>
|
||||
</div>
|
||||
<div class="review-block">
|
||||
<h3>Matched Terms</h3>
|
||||
${(Array.isArray(routing.matchedTerms) && routing.matchedTerms.length > 0)
|
||||
? `<ul>${routing.matchedTerms.map((term) => `<li>${escapePopupHtml(String(term || ""))}</li>`).join("")}</ul>`
|
||||
: "<p>No explicit terminology matches were stored.</p>"}
|
||||
</div>
|
||||
<div class="review-block">
|
||||
<h3>Likely Systems</h3>
|
||||
${(Array.isArray(routing.suggestedSystems) && routing.suggestedSystems.length > 0)
|
||||
? `<ul>${routing.suggestedSystems.map((systemId) => `<li>${escapePopupHtml(String(systemId || ""))}</li>`).join("")}</ul>`
|
||||
: "<p>No likely systems were stored.</p>"}
|
||||
</div>
|
||||
<div class="review-block">
|
||||
<h3>Possible Directions</h3>
|
||||
${(Array.isArray(routing.possibleDirections) && routing.possibleDirections.length > 0)
|
||||
? `<ul>${routing.possibleDirections.map((direction) => `<li>${escapePopupHtml(String(direction || ""))}</li>`).join("")}</ul>`
|
||||
: "<p>No alternate directions were stored.</p>"}
|
||||
</div>
|
||||
</section>
|
||||
` : ""}
|
||||
${renderedItems || '<section class="review-card"><h2>No Review Items</h2><p>This request does not have structured review details yet.</p></section>'}
|
||||
</main>
|
||||
</body>
|
||||
|
|
@ -369,6 +435,8 @@ function formatEventLabel(event: RecentSaveEvent): string {
|
|||
return "Queue worker finished";
|
||||
case "launcher-request-analysis-launch-error":
|
||||
return "Queue worker launch error";
|
||||
case "launcher-request-analysis-requeue":
|
||||
return "Request requeued for review";
|
||||
default:
|
||||
return String(event.type || "Event");
|
||||
}
|
||||
|
|
@ -401,12 +469,25 @@ function openRepo(): void {
|
|||
window.location.assign("https://repo.andraxion.net/");
|
||||
}
|
||||
|
||||
function openAdminPanelWindow(): boolean {
|
||||
const nextUrl = new URL(window.location.href);
|
||||
nextUrl.searchParams.set("admin", "requests");
|
||||
nextUrl.searchParams.set("tab", "requests");
|
||||
const popup = window.open(nextUrl.toString(), "worldshaper-admin-panel", "popup=yes,width=1620,height=980,resizable=yes,scrollbars=yes");
|
||||
if (popup) {
|
||||
popup.focus();
|
||||
}
|
||||
return Boolean(popup);
|
||||
}
|
||||
|
||||
function WorldshaperLauncher() {
|
||||
const launcherWindowMode = readLauncherWindowMode();
|
||||
const adminWindowMode = launcherWindowMode === "admin";
|
||||
const [launchState, setLaunchState] = useState<LaunchState>("ready");
|
||||
const [status, setStatus] = useState("Launch Worldshaper Studio in its floating window.");
|
||||
const [error, setError] = useState("");
|
||||
const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
||||
const [activeBoardTab, setActiveBoardTab] = useState<BoardTab>("news");
|
||||
const [activeBoardTab, setActiveBoardTab] = useState<BoardTab>(adminWindowMode ? "requests" : "news");
|
||||
const [requests, setRequests] = useState<LauncherRequest[]>([]);
|
||||
const [requestsLoading, setRequestsLoading] = useState(true);
|
||||
const [requestsError, setRequestsError] = useState("");
|
||||
|
|
@ -417,7 +498,6 @@ function WorldshaperLauncher() {
|
|||
const [requestFilter, setRequestFilter] = useState("all");
|
||||
const [allowedRequestTags, setAllowedRequestTags] = useState<string[]>([]);
|
||||
const [expandedRequestIds, setExpandedRequestIds] = useState<string[]>([]);
|
||||
const [adminPanelOpen, setAdminPanelOpen] = useState(false);
|
||||
const [adminAccessGranted, setAdminAccessGranted] = useState(false);
|
||||
const [adminPassword, setAdminPassword] = useState("");
|
||||
const [adminPasswordDraft, setAdminPasswordDraft] = useState("");
|
||||
|
|
@ -430,7 +510,9 @@ function WorldshaperLauncher() {
|
|||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [logsError, setLogsError] = useState("");
|
||||
const [queueTriggering, setQueueTriggering] = useState(false);
|
||||
const [requeueingMode, setRequeueingMode] = useState<"" | "saved" | "draft">("");
|
||||
const [adminNotice, setAdminNotice] = useState("");
|
||||
const adminPanelOpen = adminWindowMode;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
|
@ -650,15 +732,11 @@ function WorldshaperLauncher() {
|
|||
setRequestDraftOpen(false);
|
||||
setRequestsError("");
|
||||
setLogsError("");
|
||||
if (adminPanelOpen) {
|
||||
setAdminPanelOpen(false);
|
||||
setAdminNotice("");
|
||||
if (adminWindowMode) {
|
||||
return;
|
||||
}
|
||||
setAdminPanelOpen(true);
|
||||
setAdminPasswordError("");
|
||||
if (adminAccessGranted && adminPassword) {
|
||||
await refreshAdminData({ includeLogs: true, silentRequests: true });
|
||||
if (!openAdminPanelWindow()) {
|
||||
setAdminNotice("Allow popups to open the admin review window.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -831,6 +909,69 @@ function WorldshaperLauncher() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleRequeueAnalysis(mode: "saved" | "draft"): Promise<void> {
|
||||
if (!adminEditorDraft) {
|
||||
return;
|
||||
}
|
||||
setRequeueingMode(mode);
|
||||
setLogsError("");
|
||||
try {
|
||||
const payload = await fetchJsonOrThrow<RequeueAnalysisPayload>(
|
||||
`/api/launcher-requests/${encodeURIComponent(adminEditorDraft.id)}/requeue-analysis`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: buildAdminHeaders(adminPassword, {
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
mode,
|
||||
request: mode === "draft"
|
||||
? {
|
||||
title: adminEditorDraft.title,
|
||||
category: adminEditorDraft.category,
|
||||
tags: adminEditorDraft.tags,
|
||||
sourceText: adminEditorDraft.sourceText,
|
||||
summary: adminEditorDraft.summary,
|
||||
implementationNotes: adminEditorDraft.implementationNotes,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const nextRequests = Array.isArray(payload.requests) ? payload.requests : requests;
|
||||
setRequests(nextRequests);
|
||||
const refreshed = nextRequests.find((entry) => entry.id === adminEditorDraft.id) || payload.request || adminEditorDraft;
|
||||
setAdminEditorDraft(cloneLauncherRequest(refreshed));
|
||||
if (payload.launched) {
|
||||
setAdminNotice(mode === "draft"
|
||||
? "Edited draft resubmitted to the analyzer."
|
||||
: "Saved request resubmitted to the analyzer.");
|
||||
} else {
|
||||
const reason = String(payload.reason || "no-op");
|
||||
if (reason === "request-analysis-already-running") {
|
||||
setAdminNotice("The queue worker is already running. This request will be picked up on the next pass.");
|
||||
} else if (reason === "request-not-queued") {
|
||||
setAdminNotice("That request is not currently eligible for review reruns.");
|
||||
} else {
|
||||
setAdminNotice(`Review rerun returned: ${reason}.`);
|
||||
}
|
||||
}
|
||||
await refreshAdminData({ includeLogs: true, silentRequests: true });
|
||||
window.setTimeout(() => {
|
||||
void refreshAdminData({ includeLogs: true, silentRequests: true });
|
||||
}, 4200);
|
||||
} catch (nextError: unknown) {
|
||||
if (isAdminAccessError(nextError)) {
|
||||
setAdminAccessGranted(false);
|
||||
setAdminPassword("");
|
||||
setAdminPasswordError("Admin access expired. Enter the password again.");
|
||||
}
|
||||
setLogsError(String(nextError || "Failed to requeue this request for review."));
|
||||
} finally {
|
||||
setRequeueingMode("");
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleExpandedRequest(requestId: string): void {
|
||||
setExpandedRequestIds((current) => (
|
||||
current.includes(requestId)
|
||||
|
|
@ -941,16 +1082,20 @@ function WorldshaperLauncher() {
|
|||
}
|
||||
return true;
|
||||
});
|
||||
const boardHint = activeBoardTab === "news"
|
||||
? "Latest announcements"
|
||||
: `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`;
|
||||
const boardTitle = adminWindowMode ? "Worldshaper Admin" : "Worldshaper Board";
|
||||
const boardHint = adminWindowMode
|
||||
? `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`
|
||||
: (activeBoardTab === "news"
|
||||
? "Latest announcements"
|
||||
: `${queuedPendingRequestCount} queued, ${needsReviewRequestCount} review, ${activeRequestCount} active`);
|
||||
|
||||
return (
|
||||
<main
|
||||
className="launcher-shell"
|
||||
className={`launcher-shell ${adminWindowMode ? "launcher-shell-admin" : ""}`}
|
||||
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
|
||||
>
|
||||
<div className="launcher-stack">
|
||||
<div className={`launcher-stack ${adminWindowMode ? "launcher-stack-admin" : ""}`}>
|
||||
{!adminWindowMode ? (
|
||||
<section className="launcher-hero-window" aria-labelledby="launcher-studio-title">
|
||||
<div className="launcher-hero-body">
|
||||
<div className="launcher-hero-stack">
|
||||
|
|
@ -987,13 +1132,15 @@ function WorldshaperLauncher() {
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
<section className="launcher-changelog-window" aria-labelledby="launcher-board-title">
|
||||
<div className="launcher-changelog-titlebar">
|
||||
<div className="launcher-changelog-title" id="launcher-board-title">Worldshaper Board</div>
|
||||
<div className="launcher-changelog-title" id="launcher-board-title">{boardTitle}</div>
|
||||
<div className="launcher-changelog-hint">{boardHint}</div>
|
||||
</div>
|
||||
<div className="launcher-changelog-body">
|
||||
<div className="launcher-board-content">
|
||||
{!adminWindowMode ? (
|
||||
<div className="launcher-board-tabs">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1010,6 +1157,7 @@ function WorldshaperLauncher() {
|
|||
Requests
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{activeBoardTab === "news" ? (
|
||||
<div className="changelog-splash-card">
|
||||
<div className="changelog-splash-hero">
|
||||
|
|
@ -1046,8 +1194,8 @@ function WorldshaperLauncher() {
|
|||
) : (
|
||||
<div className="changelog-splash-card">
|
||||
<div className="changelog-splash-hero">
|
||||
<div className="changelog-splash-kicker">Shared Request Board</div>
|
||||
<div className="changelog-splash-title" id="launcher-requests-title">Requests</div>
|
||||
<div className="changelog-splash-kicker">{adminWindowMode ? "Protected Review Workspace" : "Shared Request Board"}</div>
|
||||
<div className="changelog-splash-title" id="launcher-requests-title">{adminWindowMode ? "Admin Review Console" : "Requests"}</div>
|
||||
<div className="changelog-splash-meta">
|
||||
{requestCount} saved request{requestCount === 1 ? "" : "s"}: {queuedPendingRequestCount} queued, {needsReviewRequestCount} in review, and {activeRequestCount} active.
|
||||
</div>
|
||||
|
|
@ -1057,20 +1205,21 @@ function WorldshaperLauncher() {
|
|||
type="button"
|
||||
className="launcher-primary-btn"
|
||||
onClick={() => {
|
||||
setAdminPanelOpen(false);
|
||||
setRequestDraftOpen((value) => !value);
|
||||
}}
|
||||
disabled={requestSubmitting}
|
||||
>
|
||||
{requestDraftOpen && !adminPanelOpen ? "Hide Request Form" : "Add New Request"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`launcher-secondary-btn ${adminPanelOpen ? "is-active" : ""}`}
|
||||
onClick={() => void handleAdminPanelToggle()}
|
||||
>
|
||||
{adminPanelOpen ? "Hide Admin Panel" : "Admin Panel"}
|
||||
</button>
|
||||
{!adminPanelOpen ? (
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-secondary-btn"
|
||||
onClick={() => void handleAdminPanelToggle()}
|
||||
>
|
||||
Open Admin Window
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<label className="launcher-request-filter">
|
||||
<span className="launcher-request-filter-label">Filter</span>
|
||||
|
|
@ -1196,6 +1345,9 @@ function WorldshaperLauncher() {
|
|||
{reviewItem?.reviewRationale ? (
|
||||
<div className="launcher-request-admin-request-rationale">{reviewItem.reviewRationale}</div>
|
||||
) : null}
|
||||
{!reviewItem?.reviewRationale && requestEntry.analysis?.routing?.summary ? (
|
||||
<div className="launcher-request-admin-request-rationale">{requestEntry.analysis.routing.summary}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1290,6 +1442,140 @@ function WorldshaperLauncher() {
|
|||
})}
|
||||
</div>
|
||||
</label>
|
||||
<section className="launcher-request-admin-analysis-item">
|
||||
<div className="launcher-request-admin-analysis-head">
|
||||
<div>
|
||||
<div className="launcher-request-admin-kicker">Routing Pass</div>
|
||||
<div className="launcher-request-admin-request-title">KB Routing Summary</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="launcher-request-admin-editor-grid">
|
||||
<label className="launcher-request-admin-field">
|
||||
<span className="launcher-request-filter-label">Ambiguity</span>
|
||||
<select
|
||||
className="launcher-request-filter-select"
|
||||
value={String(adminEditorDraft.analysis?.routing?.ambiguity || "medium")}
|
||||
onChange={(event) => updateAdminDraft((current) => ({
|
||||
...current,
|
||||
analysis: {
|
||||
...(current.analysis || {}),
|
||||
routing: {
|
||||
...(current.analysis?.routing || {}),
|
||||
ambiguity: event.target.value as "low" | "medium" | "high",
|
||||
},
|
||||
},
|
||||
}))}
|
||||
>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="launcher-request-admin-field">
|
||||
<span className="launcher-request-filter-label">Suggested Tags</span>
|
||||
<input
|
||||
type="text"
|
||||
className="launcher-request-filter-select"
|
||||
value={Array.isArray(adminEditorDraft.analysis?.routing?.suggestedTags) ? adminEditorDraft.analysis?.routing?.suggestedTags?.join(", ") : ""}
|
||||
onChange={(event) => updateAdminDraft((current) => ({
|
||||
...current,
|
||||
analysis: {
|
||||
...(current.analysis || {}),
|
||||
routing: {
|
||||
...(current.analysis?.routing || {}),
|
||||
suggestedTags: event.target.value.split(",").map((entry) => entry.trim()).filter(Boolean),
|
||||
},
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="launcher-request-admin-field">
|
||||
<span className="launcher-request-filter-label">Routing Summary</span>
|
||||
<textarea
|
||||
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
|
||||
value={String(adminEditorDraft.analysis?.routing?.summary || "")}
|
||||
onChange={(event) => updateAdminDraft((current) => ({
|
||||
...current,
|
||||
analysis: {
|
||||
...(current.analysis || {}),
|
||||
routing: {
|
||||
...(current.analysis?.routing || {}),
|
||||
summary: event.target.value,
|
||||
},
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
<label className="launcher-request-admin-field">
|
||||
<span className="launcher-request-filter-label">Routing Rationale</span>
|
||||
<textarea
|
||||
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
|
||||
value={String(adminEditorDraft.analysis?.routing?.rationale || "")}
|
||||
onChange={(event) => updateAdminDraft((current) => ({
|
||||
...current,
|
||||
analysis: {
|
||||
...(current.analysis || {}),
|
||||
routing: {
|
||||
...(current.analysis?.routing || {}),
|
||||
rationale: event.target.value,
|
||||
},
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
<label className="launcher-request-admin-field">
|
||||
<span className="launcher-request-filter-label">Matched Terms</span>
|
||||
<textarea
|
||||
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
|
||||
value={Array.isArray(adminEditorDraft.analysis?.routing?.matchedTerms) ? adminEditorDraft.analysis?.routing?.matchedTerms?.join("\n") : ""}
|
||||
onChange={(event) => updateAdminDraft((current) => ({
|
||||
...current,
|
||||
analysis: {
|
||||
...(current.analysis || {}),
|
||||
routing: {
|
||||
...(current.analysis?.routing || {}),
|
||||
matchedTerms: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
|
||||
},
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
<label className="launcher-request-admin-field">
|
||||
<span className="launcher-request-filter-label">Likely Systems</span>
|
||||
<textarea
|
||||
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
|
||||
value={Array.isArray(adminEditorDraft.analysis?.routing?.suggestedSystems) ? adminEditorDraft.analysis?.routing?.suggestedSystems?.join("\n") : ""}
|
||||
onChange={(event) => updateAdminDraft((current) => ({
|
||||
...current,
|
||||
analysis: {
|
||||
...(current.analysis || {}),
|
||||
routing: {
|
||||
...(current.analysis?.routing || {}),
|
||||
suggestedSystems: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
|
||||
},
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
<label className="launcher-request-admin-field">
|
||||
<span className="launcher-request-filter-label">Possible Directions</span>
|
||||
<textarea
|
||||
className="launcher-request-textarea launcher-request-admin-textarea launcher-request-admin-textarea-sm"
|
||||
value={Array.isArray(adminEditorDraft.analysis?.routing?.possibleDirections) ? adminEditorDraft.analysis?.routing?.possibleDirections?.join("\n") : ""}
|
||||
onChange={(event) => updateAdminDraft((current) => ({
|
||||
...current,
|
||||
analysis: {
|
||||
...(current.analysis || {}),
|
||||
routing: {
|
||||
...(current.analysis?.routing || {}),
|
||||
possibleDirections: event.target.value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean),
|
||||
},
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
{(adminEditorDraft.analysis?.items || []).map((item, itemIndex) => (
|
||||
<section key={`analysis-item-${itemIndex}`} className="launcher-request-admin-analysis-item">
|
||||
<div className="launcher-request-admin-analysis-head">
|
||||
|
|
@ -1445,15 +1731,31 @@ function WorldshaperLauncher() {
|
|||
type="button"
|
||||
className="launcher-primary-btn"
|
||||
onClick={() => void handleSaveAdminRequest()}
|
||||
disabled={adminSaving}
|
||||
disabled={adminSaving || requeueingMode !== ""}
|
||||
>
|
||||
{adminSaving ? "Saving..." : "Save Review Changes"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-secondary-btn"
|
||||
onClick={() => void handleRequeueAnalysis("saved")}
|
||||
disabled={adminSaving || requeueingMode !== ""}
|
||||
>
|
||||
{requeueingMode === "saved" ? "Re-running Saved Review..." : "Review Saved Request"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-secondary-btn"
|
||||
onClick={() => void handleRequeueAnalysis("draft")}
|
||||
disabled={adminSaving || requeueingMode !== ""}
|
||||
>
|
||||
{requeueingMode === "draft" ? "Submitting Draft Review..." : "Review Edited Draft"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="launcher-secondary-btn"
|
||||
onClick={() => void handleApproveAdminRequest()}
|
||||
disabled={adminSaving}
|
||||
disabled={adminSaving || requeueingMode !== ""}
|
||||
>
|
||||
Approve Request
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ body {
|
|||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.launcher-shell-admin {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.launcher-shell::before,
|
||||
.launcher-shell::after {
|
||||
content: "";
|
||||
|
|
@ -69,6 +73,10 @@ body {
|
|||
gap: 18px;
|
||||
}
|
||||
|
||||
.launcher-stack-admin {
|
||||
width: min(1440px, 100%);
|
||||
}
|
||||
|
||||
.launcher-hero-window,
|
||||
.launcher-changelog-window {
|
||||
border: 1px solid #4f79af;
|
||||
|
|
@ -161,6 +169,10 @@ body {
|
|||
width: min(760px, 100%);
|
||||
}
|
||||
|
||||
.launcher-shell-admin .launcher-changelog-window {
|
||||
width: min(1440px, 100%);
|
||||
}
|
||||
|
||||
.launcher-changelog-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -192,6 +204,10 @@ body {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.launcher-shell-admin .launcher-changelog-body {
|
||||
max-height: calc(100dvh - 92px);
|
||||
}
|
||||
|
||||
.launcher-board-content {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
|
|
@ -400,6 +416,10 @@ body {
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.launcher-shell-admin .launcher-request-admin-grid {
|
||||
grid-template-columns: 360px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.launcher-request-admin-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
|
|
@ -441,6 +461,11 @@ body {
|
|||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.launcher-shell-admin .launcher-request-admin-request-list,
|
||||
.launcher-shell-admin .launcher-request-admin-log-list {
|
||||
max-height: 42dvh;
|
||||
}
|
||||
|
||||
.launcher-request-admin-request-row,
|
||||
.launcher-request-admin-log-row {
|
||||
display: grid;
|
||||
|
|
@ -532,6 +557,10 @@ body {
|
|||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.launcher-shell-admin .launcher-request-admin-editor {
|
||||
max-height: 68dvh;
|
||||
}
|
||||
|
||||
.launcher-request-admin-editor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue