@agile-vibe-coding/avc 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +152 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +559 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +143 -0
- package/cli/agents/mission-scope-validator.md +146 -0
- package/cli/agents/project-context-extractor.md +122 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +183 -0
- package/cli/agents/validator-documentation.md +455 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/api-reference-tool.js +368 -0
- package/cli/build-docs.js +29 -8
- package/cli/ceremony-history.js +369 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/command-logger.js +49 -12
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +659 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/init-model-config.js +704 -0
- package/cli/init.js +1737 -278
- package/cli/kanban-server-manager.js +227 -0
- package/cli/llm-claude.js +150 -1
- package/cli/llm-gemini.js +109 -0
- package/cli/llm-local.js +493 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +454 -0
- package/cli/llm-provider.js +379 -3
- package/cli/llm-token-limits.js +211 -0
- package/cli/llm-verifier.js +662 -0
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +49 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +291 -0
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +192 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +270 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +73 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +4625 -1094
- package/cli/repl-old.js +3 -4
- package/cli/seed-processor.js +962 -0
- package/cli/sprint-planning-processor.js +4162 -0
- package/cli/template-processor.js +2149 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- package/cli/token-tracker.js +547 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +667 -0
- package/cli/verification-tracker.js +563 -0
- package/cli/worktree-runner.js +654 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +651 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +384 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +177 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +515 -0
- package/kanban/client/src/lib/status-grouping.js +154 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +123 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +537 -0
- package/kanban/server/routes/ceremony.js +454 -0
- package/kanban/server/routes/costs.js +163 -0
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +736 -0
- package/kanban/server/routes/websocket.js +281 -0
- package/kanban/server/routes/work-items.js +487 -0
- package/kanban/server/services/CeremonyService.js +1441 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +92 -0
- package/kanban/server/workers/sprint-planning-worker.js +212 -0
- package/package.json +19 -7
- package/cli/agents/documentation.md +0 -302
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ceremony Router
|
|
7
|
+
* Handles all /api/ceremony/* endpoints for the sponsor-call wizard.
|
|
8
|
+
* @param {CeremonyService} ceremonyService
|
|
9
|
+
*/
|
|
10
|
+
export function createCeremonyRouter(ceremonyService, processRegistry, taskRunnerService = null) {
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
|
|
13
|
+
// GET /api/ceremony/status — current ceremony state
|
|
14
|
+
router.get('/status', (req, res) => {
|
|
15
|
+
res.json(ceremonyService.getStatus());
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// GET /api/ceremony/models — available LLM models with API key status
|
|
19
|
+
router.get('/models', async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const models = await ceremonyService.getAvailableModels();
|
|
22
|
+
res.json(models);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error('getAvailableModels error:', err);
|
|
25
|
+
res.status(500).json({ error: err.message });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// POST /api/ceremony/generate-mission
|
|
30
|
+
// Body: { description, modelId, provider, validatorModelId, validatorProvider }
|
|
31
|
+
router.post('/generate-mission', async (req, res) => {
|
|
32
|
+
const { description, modelId, provider, validatorModelId, validatorProvider } = req.body;
|
|
33
|
+
console.log('[ceremony] POST /generate-mission', {
|
|
34
|
+
descriptionLength: description?.length,
|
|
35
|
+
modelId,
|
|
36
|
+
provider,
|
|
37
|
+
validatorModelId,
|
|
38
|
+
validatorProvider,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!description?.trim() || !modelId || !provider || !validatorModelId || !validatorProvider) {
|
|
42
|
+
console.warn('[ceremony] generate-mission: missing required fields');
|
|
43
|
+
return res.status(400).json({
|
|
44
|
+
error: 'description, modelId, provider, validatorModelId and validatorProvider are required',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
try {
|
|
50
|
+
const result = await ceremonyService.generateMissionScope(
|
|
51
|
+
description, modelId, provider, validatorModelId, validatorProvider
|
|
52
|
+
);
|
|
53
|
+
console.log(`[ceremony] generate-mission completed in ${Date.now() - start}ms`, {
|
|
54
|
+
validationScore: result.validationScore,
|
|
55
|
+
iterations: result.iterations,
|
|
56
|
+
issueCount: result.issues?.length,
|
|
57
|
+
});
|
|
58
|
+
res.json(result);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`[ceremony] generate-mission failed in ${Date.now() - start}ms:`, err.message);
|
|
61
|
+
res.status(500).json({ error: err.message });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// POST /api/ceremony/refine-mission
|
|
66
|
+
// Body: { missionStatement, initialScope, refinementRequest, modelId, provider, validatorModelId, validatorProvider }
|
|
67
|
+
router.post('/refine-mission', async (req, res) => {
|
|
68
|
+
const { missionStatement, initialScope, refinementRequest, modelId, provider, validatorModelId, validatorProvider } = req.body;
|
|
69
|
+
if (!missionStatement?.trim() || !initialScope?.trim() || !refinementRequest?.trim() || !modelId || !provider || !validatorModelId || !validatorProvider) {
|
|
70
|
+
return res.status(400).json({ error: 'missionStatement, initialScope, refinementRequest, modelId, provider, validatorModelId and validatorProvider are required' });
|
|
71
|
+
}
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
try {
|
|
74
|
+
const result = await ceremonyService.refineMissionScope(
|
|
75
|
+
missionStatement, initialScope, refinementRequest, modelId, provider, validatorModelId, validatorProvider
|
|
76
|
+
);
|
|
77
|
+
console.log(`[ceremony] refine-mission completed in ${Date.now() - start}ms`, { validationScore: result.validationScore, iterations: result.iterations });
|
|
78
|
+
res.json(result);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('[ceremony] refine-mission failed:', err.message);
|
|
81
|
+
res.status(500).json({ error: err.message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// POST /api/ceremony/generate-architecture
|
|
86
|
+
// Body: { description, modelId, provider }
|
|
87
|
+
router.post('/generate-architecture', async (req, res) => {
|
|
88
|
+
const { description, modelId, provider } = req.body;
|
|
89
|
+
if (!description?.trim() || !modelId || !provider) {
|
|
90
|
+
return res.status(400).json({ error: 'description, modelId and provider are required' });
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const arch = await ceremonyService.generateCustomArchitecture(description, modelId, provider);
|
|
94
|
+
res.json(arch);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('[ceremony] generate-architecture failed:', err.message);
|
|
97
|
+
res.status(500).json({ error: err.message });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// POST /api/ceremony/refine-architecture
|
|
102
|
+
// Body: { currentArch, refinementRequest, modelId, provider }
|
|
103
|
+
router.post('/refine-architecture', async (req, res) => {
|
|
104
|
+
const { currentArch, refinementRequest, modelId, provider } = req.body;
|
|
105
|
+
if (!currentArch || !refinementRequest?.trim() || !modelId || !provider) {
|
|
106
|
+
return res.status(400).json({ error: 'currentArch, refinementRequest, modelId and provider are required' });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const arch = await ceremonyService.refineCustomArchitecture(currentArch, refinementRequest, modelId, provider);
|
|
110
|
+
res.json(arch);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('[ceremony] refine-architecture failed:', err.message);
|
|
113
|
+
res.status(500).json({ error: err.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// POST /api/ceremony/analyze/database
|
|
118
|
+
// Body: { mission, scope, strategy }
|
|
119
|
+
router.post('/analyze/database', async (req, res) => {
|
|
120
|
+
const { mission, scope, strategy } = req.body;
|
|
121
|
+
console.log('[ceremony] POST /analyze/database', {
|
|
122
|
+
missionLength: mission?.length,
|
|
123
|
+
scopeLines: scope?.split('\n').length,
|
|
124
|
+
strategy,
|
|
125
|
+
});
|
|
126
|
+
if (!mission || !scope) {
|
|
127
|
+
return res.status(400).json({ error: 'mission and scope are required' });
|
|
128
|
+
}
|
|
129
|
+
const start = Date.now();
|
|
130
|
+
try {
|
|
131
|
+
const result = await ceremonyService.analyzeDatabase(mission, scope, strategy || null);
|
|
132
|
+
console.log(`[ceremony] analyze/database completed in ${Date.now() - start}ms`);
|
|
133
|
+
res.json(result);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`[ceremony] analyze/database failed in ${Date.now() - start}ms:`, err.message);
|
|
136
|
+
res.status(500).json({ error: err.message });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// POST /api/ceremony/analyze/architecture
|
|
141
|
+
// Body: { mission, scope, dbContext, strategy }
|
|
142
|
+
router.post('/analyze/architecture', async (req, res) => {
|
|
143
|
+
const { mission, scope, dbContext, strategy } = req.body;
|
|
144
|
+
console.log('[ceremony] POST /analyze/architecture', {
|
|
145
|
+
missionLength: mission?.length,
|
|
146
|
+
dbContext: dbContext ? 'provided' : 'null',
|
|
147
|
+
strategy,
|
|
148
|
+
});
|
|
149
|
+
if (!mission || !scope) {
|
|
150
|
+
return res.status(400).json({ error: 'mission and scope are required' });
|
|
151
|
+
}
|
|
152
|
+
const start = Date.now();
|
|
153
|
+
try {
|
|
154
|
+
const result = await ceremonyService.analyzeArchitecture(
|
|
155
|
+
mission,
|
|
156
|
+
scope,
|
|
157
|
+
dbContext || null,
|
|
158
|
+
strategy || null
|
|
159
|
+
);
|
|
160
|
+
console.log(`[ceremony] analyze/architecture completed in ${Date.now() - start}ms`);
|
|
161
|
+
res.json(result);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`[ceremony] analyze/architecture failed in ${Date.now() - start}ms:`, err.message);
|
|
164
|
+
res.status(500).json({ error: err.message });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// POST /api/ceremony/analyze/prefill
|
|
169
|
+
// Body: { mission, scope, arch, dbContext, strategy }
|
|
170
|
+
router.post('/analyze/prefill', async (req, res) => {
|
|
171
|
+
const { mission, scope, arch, dbContext, strategy } = req.body;
|
|
172
|
+
console.log('[ceremony] POST /analyze/prefill', {
|
|
173
|
+
missionLength: mission?.length,
|
|
174
|
+
arch: arch ? 'provided' : 'null',
|
|
175
|
+
strategy,
|
|
176
|
+
});
|
|
177
|
+
if (!mission || !scope || !arch) {
|
|
178
|
+
return res.status(400).json({ error: 'mission, scope, and arch are required' });
|
|
179
|
+
}
|
|
180
|
+
const start = Date.now();
|
|
181
|
+
try {
|
|
182
|
+
const result = await ceremonyService.prefillAnswers(
|
|
183
|
+
mission,
|
|
184
|
+
scope,
|
|
185
|
+
arch,
|
|
186
|
+
dbContext || null,
|
|
187
|
+
strategy || null
|
|
188
|
+
);
|
|
189
|
+
console.log(`[ceremony] analyze/prefill completed in ${Date.now() - start}ms`);
|
|
190
|
+
res.json(result);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error(`[ceremony] analyze/prefill failed in ${Date.now() - start}ms:`, err.message);
|
|
193
|
+
res.status(500).json({ error: err.message });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// POST /api/ceremony/refine-field
|
|
198
|
+
// Body: { fieldKey, fieldLabel, currentValue, refinementRequest, context: { mission, scope }, modelId, provider }
|
|
199
|
+
router.post('/refine-field', async (req, res) => {
|
|
200
|
+
const { fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider } = req.body;
|
|
201
|
+
if (!fieldKey || !currentValue?.trim() || !refinementRequest?.trim() || !context?.mission || !modelId || !provider) {
|
|
202
|
+
return res.status(400).json({ error: 'fieldKey, currentValue, refinementRequest, context.mission, modelId and provider are required' });
|
|
203
|
+
}
|
|
204
|
+
const start = Date.now();
|
|
205
|
+
try {
|
|
206
|
+
const result = await ceremonyService.refineField(fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider);
|
|
207
|
+
console.log(`[ceremony] refine-field completed in ${Date.now() - start}ms`, { fieldKey });
|
|
208
|
+
res.json(result);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error(`[ceremony] refine-field failed in ${Date.now() - start}ms:`, err.message);
|
|
211
|
+
res.status(500).json({ error: err.message });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// POST /api/ceremony/run
|
|
216
|
+
// Body: { requirements } — all 7 template variables
|
|
217
|
+
router.post('/run', async (req, res) => {
|
|
218
|
+
const { requirements } = req.body;
|
|
219
|
+
console.log('[ceremony] POST /run', {
|
|
220
|
+
requirementKeys: Object.keys(requirements || {}),
|
|
221
|
+
missionLength: requirements?.MISSION_STATEMENT?.length,
|
|
222
|
+
});
|
|
223
|
+
if (!requirements || !requirements.MISSION_STATEMENT) {
|
|
224
|
+
return res.status(400).json({ error: 'requirements.MISSION_STATEMENT is required' });
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const processId = await ceremonyService.runSponsorCallInProcess(processRegistry, requirements);
|
|
228
|
+
console.log('[ceremony] run started in process', processId);
|
|
229
|
+
res.json({ started: true, processId });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error('[ceremony] run error:', err.message);
|
|
232
|
+
res.status(500).json({ error: err.message });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// GET /api/ceremony/sprint-planning/resumable — check if previous run can be resumed
|
|
237
|
+
router.get('/sprint-planning/resumable', async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
const { CeremonyHistory } = await import('../../../cli/ceremony-history.js');
|
|
240
|
+
const historyPath = path.join(ceremonyService.projectRoot, '.avc');
|
|
241
|
+
const history = new CeremonyHistory(historyPath);
|
|
242
|
+
history.init();
|
|
243
|
+
|
|
244
|
+
const resumableStages = ['files-written', 'docs-generated', 'enrichment-complete'];
|
|
245
|
+
const ceremonies = history.data?.ceremonies?.['sprint-planning'];
|
|
246
|
+
const executions = ceremonies?.executions || [];
|
|
247
|
+
const last = executions[executions.length - 1];
|
|
248
|
+
|
|
249
|
+
if (!last) return res.json({ resumable: false });
|
|
250
|
+
|
|
251
|
+
const isAborted = last.status === 'aborted' || last.outcome === 'abrupt-termination' ||
|
|
252
|
+
(last.status === 'in-progress');
|
|
253
|
+
const hasResumableCheckpoint = resumableStages.includes(last.stage);
|
|
254
|
+
|
|
255
|
+
if (!isAborted || !hasResumableCheckpoint) {
|
|
256
|
+
return res.json({ resumable: false, reason: isAborted ? 'checkpoint-too-early' : 'not-aborted' });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Validate files exist on disk
|
|
260
|
+
const projectDir = path.join(ceremonyService.projectRoot, '.avc', 'project');
|
|
261
|
+
const epicDirs = fs.existsSync(projectDir)
|
|
262
|
+
? fs.readdirSync(projectDir).filter(d => d.startsWith('context-') && fs.existsSync(path.join(projectDir, d, 'work.json')))
|
|
263
|
+
: [];
|
|
264
|
+
|
|
265
|
+
if (epicDirs.length === 0) {
|
|
266
|
+
return res.json({ resumable: false, reason: 'no-files-on-disk' });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const stageLabels = {
|
|
270
|
+
'files-written': 'File Writing (doc generation pending)',
|
|
271
|
+
'docs-generated': 'Doc Generation (enrichment pending)',
|
|
272
|
+
'enrichment-complete': 'Enrichment (summary pending)',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
res.json({
|
|
276
|
+
resumable: true,
|
|
277
|
+
checkpoint: last.stage,
|
|
278
|
+
checkpointLabel: stageLabels[last.stage] || last.stage,
|
|
279
|
+
executionId: last.id,
|
|
280
|
+
timestamp: last.lastCheckpoint || last.startTime,
|
|
281
|
+
epics: epicDirs.length,
|
|
282
|
+
});
|
|
283
|
+
} catch (err) {
|
|
284
|
+
res.json({ resumable: false, reason: err.message });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// POST /api/ceremony/sprint-planning/run
|
|
289
|
+
router.post('/sprint-planning/run', async (req, res) => {
|
|
290
|
+
try {
|
|
291
|
+
const { resumeFrom } = req.body || {};
|
|
292
|
+
const processId = await ceremonyService.runSprintPlanningInProcess(processRegistry, resumeFrom || null);
|
|
293
|
+
console.log('[ceremony] sprint-planning/run started in process', processId, resumeFrom ? `(resume from ${resumeFrom})` : '');
|
|
294
|
+
res.json({ started: true, processId });
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error('[ceremony] sprint-planning/run error:', err.message);
|
|
297
|
+
res.status(400).json({ error: err.message });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// POST /api/ceremony/sprint-planning/confirm-selection
|
|
302
|
+
// Body: { selectedEpicIds: string[], selectedStoryIds: string[] }
|
|
303
|
+
router.post('/sprint-planning/confirm-selection', (req, res) => {
|
|
304
|
+
const { selectedEpicIds, selectedStoryIds } = req.body;
|
|
305
|
+
if (!Array.isArray(selectedEpicIds) || !Array.isArray(selectedStoryIds)) {
|
|
306
|
+
return res.status(400).json({ error: 'selectedEpicIds and selectedStoryIds must be arrays' });
|
|
307
|
+
}
|
|
308
|
+
ceremonyService.confirmSprintPlanningSelection(selectedEpicIds, selectedStoryIds);
|
|
309
|
+
res.json({ ok: true });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// POST /api/ceremony/pause
|
|
313
|
+
router.post('/pause', (req, res) => {
|
|
314
|
+
ceremonyService.pause();
|
|
315
|
+
res.json({ ok: true });
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// POST /api/ceremony/resume
|
|
319
|
+
router.post('/resume', (req, res) => {
|
|
320
|
+
ceremonyService.resume();
|
|
321
|
+
res.json({ ok: true });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// POST /api/ceremony/cancel
|
|
325
|
+
router.post('/cancel', (req, res) => {
|
|
326
|
+
const keepItems = req.body?.keepItems === true;
|
|
327
|
+
ceremonyService.cancel({ keepItems });
|
|
328
|
+
res.json({ ok: true });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// POST /api/ceremony/cost-limit-continue
|
|
332
|
+
router.post('/cost-limit-continue', (req, res) => {
|
|
333
|
+
ceremonyService.continuePastCostLimit();
|
|
334
|
+
res.json({ ok: true });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// POST /api/ceremony/quota-continue
|
|
338
|
+
// Body: { newProvider?: string, newModel?: string }
|
|
339
|
+
// Omit newProvider to retry with same model (e.g. user added credits).
|
|
340
|
+
router.post('/quota-continue', (req, res) => {
|
|
341
|
+
const { newProvider = null, newModel = null } = req.body || {};
|
|
342
|
+
ceremonyService.continueAfterQuota(newProvider, newModel);
|
|
343
|
+
res.json({ ok: true });
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// POST /api/ceremony/reset — force-stop any running ceremony and reset state immediately
|
|
347
|
+
router.post('/reset', (req, res) => {
|
|
348
|
+
ceremonyService.forceReset();
|
|
349
|
+
res.json({ ok: true });
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ── Sponsor-call draft (resume support) ────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
const draftFilePath = () =>
|
|
355
|
+
path.join(ceremonyService.projectRoot, '.avc', 'sponsor-call-draft.json');
|
|
356
|
+
|
|
357
|
+
// GET /api/ceremony/sponsor-call/draft
|
|
358
|
+
router.get('/sponsor-call/draft', (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
const content = fs.readFileSync(draftFilePath(), 'utf8');
|
|
361
|
+
res.json(JSON.parse(content));
|
|
362
|
+
} catch (_) {
|
|
363
|
+
res.status(404).json({ draft: null });
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// PUT /api/ceremony/sponsor-call/draft
|
|
368
|
+
router.put('/sponsor-call/draft', (req, res) => {
|
|
369
|
+
try {
|
|
370
|
+
const data = { ...req.body, savedAt: new Date().toISOString() };
|
|
371
|
+
fs.writeFileSync(draftFilePath(), JSON.stringify(data, null, 2));
|
|
372
|
+
res.json({ ok: true });
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.error('[ceremony] draft save error:', err.message);
|
|
375
|
+
res.status(500).json({ error: err.message });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// DELETE /api/ceremony/sponsor-call/draft
|
|
380
|
+
router.delete('/sponsor-call/draft', (req, res) => {
|
|
381
|
+
try { fs.unlinkSync(draftFilePath()); } catch (_) {}
|
|
382
|
+
res.json({ ok: true });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ---- Task Runner: Seed + Run ----
|
|
386
|
+
|
|
387
|
+
// POST /api/ceremony/seed/run — seed a story into tasks/subtasks
|
|
388
|
+
router.post('/seed/run', async (req, res) => {
|
|
389
|
+
if (!taskRunnerService) {
|
|
390
|
+
return res.status(503).json({ error: 'TaskRunnerService not available' });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { storyId } = req.body;
|
|
394
|
+
if (!storyId || !/^context-\d{4}-\d{4}[a-z]?$/.test(storyId)) {
|
|
395
|
+
return res.status(400).json({ error: 'Valid storyId is required (format: context-XXXX-XXXX)' });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (taskRunnerService.isRunning(storyId)) {
|
|
399
|
+
return res.status(409).json({ error: `Seed already running for ${storyId}` });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const processId = taskRunnerService.runSeed(storyId);
|
|
404
|
+
res.json({ started: true, processId, storyId });
|
|
405
|
+
} catch (err) {
|
|
406
|
+
res.status(500).json({ error: err.message });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// POST /api/ceremony/run-task/run — run a task in a worktree
|
|
411
|
+
router.post('/run-task/run', async (req, res) => {
|
|
412
|
+
if (!taskRunnerService) {
|
|
413
|
+
return res.status(503).json({ error: 'TaskRunnerService not available' });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const { taskId } = req.body;
|
|
417
|
+
if (!taskId || !/^context-\d{4}-\d{4}[a-z]?-\d{4}$/.test(taskId)) {
|
|
418
|
+
return res.status(400).json({ error: 'Valid taskId is required (format: context-XXXX-XXXX-XXXX)' });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (taskRunnerService.isRunning(taskId)) {
|
|
422
|
+
return res.status(409).json({ error: `Run already active for ${taskId}` });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const processId = taskRunnerService.runTask(taskId);
|
|
427
|
+
res.json({ started: true, processId, taskId });
|
|
428
|
+
} catch (err) {
|
|
429
|
+
res.status(500).json({ error: err.message });
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// GET /api/ceremony/task-runner/status — list all active runs
|
|
434
|
+
router.get('/task-runner/status', (req, res) => {
|
|
435
|
+
if (!taskRunnerService) {
|
|
436
|
+
return res.status(503).json({ error: 'TaskRunnerService not available' });
|
|
437
|
+
}
|
|
438
|
+
res.json({ runs: taskRunnerService.listRuns() });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// POST /api/ceremony/task-runner/cancel — cancel a run
|
|
442
|
+
router.post('/task-runner/cancel', (req, res) => {
|
|
443
|
+
if (!taskRunnerService) {
|
|
444
|
+
return res.status(503).json({ error: 'TaskRunnerService not available' });
|
|
445
|
+
}
|
|
446
|
+
const { processId } = req.body;
|
|
447
|
+
if (!processId) return res.status(400).json({ error: 'processId is required' });
|
|
448
|
+
|
|
449
|
+
const cancelled = taskRunnerService.cancel(processId);
|
|
450
|
+
res.json({ cancelled });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return router;
|
|
454
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Costs Router
|
|
7
|
+
* Handles GET /api/costs/summary and /api/costs/history
|
|
8
|
+
* Reads token-history.json written by TokenTracker.
|
|
9
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
10
|
+
*/
|
|
11
|
+
export function createCostsRouter(projectRoot) {
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
const historyPath = path.join(projectRoot, '.avc', 'token-history.json');
|
|
14
|
+
|
|
15
|
+
// Top-level ceremony names — each becomes a parent node in the hierarchy
|
|
16
|
+
const PARENT_CEREMONIES = ['sponsor-call', 'sprint-planning', 'seed'];
|
|
17
|
+
|
|
18
|
+
// Explicit stage → parent mapping for stages whose names don't carry a prefix
|
|
19
|
+
const STAGE_PARENT_MAP = {
|
|
20
|
+
'mission-scope': 'sponsor-call',
|
|
21
|
+
'mission-refine': 'sponsor-call',
|
|
22
|
+
'analyze-database': 'sponsor-call',
|
|
23
|
+
'analyze-architecture': 'sponsor-call',
|
|
24
|
+
'prefill-answers': 'sponsor-call',
|
|
25
|
+
'sprint-planning-decomposition': 'sprint-planning',
|
|
26
|
+
'sprint-planning-validation': 'sprint-planning',
|
|
27
|
+
'sprint-planning-solver': 'sprint-planning',
|
|
28
|
+
'sprint-planning-doc-distribution':'sprint-planning',
|
|
29
|
+
'sprint-planning-enrichment': 'sprint-planning',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function getParentCeremony(key) {
|
|
33
|
+
if (STAGE_PARENT_MAP[key]) return STAGE_PARENT_MAP[key];
|
|
34
|
+
for (const parent of PARENT_CEREMONIES) {
|
|
35
|
+
if (key !== parent && key.startsWith(`${parent}-`)) return parent;
|
|
36
|
+
}
|
|
37
|
+
if (PARENT_CEREMONIES.includes(key)) return key; // self
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readHistory() {
|
|
42
|
+
if (!fs.existsSync(historyPath)) return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(historyPath, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getCurrentMonthKey() {
|
|
51
|
+
return new Date().toISOString().substring(0, 7); // YYYY-MM
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// GET /api/costs/summary — current month totals for header chip
|
|
55
|
+
router.get('/summary', (req, res) => {
|
|
56
|
+
const history = readHistory();
|
|
57
|
+
if (!history) return res.json({ totalCost: 0, totalTokens: 0, apiCalls: 0 });
|
|
58
|
+
|
|
59
|
+
const monthKey = getCurrentMonthKey();
|
|
60
|
+
const monthly = history.totals?.monthly?.[monthKey] ?? {};
|
|
61
|
+
|
|
62
|
+
res.json({
|
|
63
|
+
totalCost: monthly.cost?.total ?? 0,
|
|
64
|
+
totalTokens: (monthly.input ?? 0) + (monthly.output ?? 0),
|
|
65
|
+
apiCalls: monthly.executions ?? 0,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// GET /api/costs/history?days=30 (or ?from=YYYY-MM-DD&to=YYYY-MM-DD)
|
|
70
|
+
router.get('/history', (req, res) => {
|
|
71
|
+
const history = readHistory();
|
|
72
|
+
if (!history) return res.json({ daily: [], ceremonies: [] });
|
|
73
|
+
|
|
74
|
+
// Determine date range
|
|
75
|
+
let cutoff, endDate;
|
|
76
|
+
if (req.query.from && req.query.to) {
|
|
77
|
+
cutoff = new Date(req.query.from);
|
|
78
|
+
endDate = new Date(req.query.to);
|
|
79
|
+
endDate.setDate(endDate.getDate() + 1); // inclusive end
|
|
80
|
+
} else {
|
|
81
|
+
const days = Math.max(1, Math.min(365, parseInt(req.query.days ?? '30', 10)));
|
|
82
|
+
cutoff = new Date();
|
|
83
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
84
|
+
endDate = new Date();
|
|
85
|
+
endDate.setDate(endDate.getDate() + 1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Filter daily totals
|
|
89
|
+
const dailyData = history.totals?.daily ?? {};
|
|
90
|
+
const daily = Object.entries(dailyData)
|
|
91
|
+
.filter(([date]) => {
|
|
92
|
+
const d = new Date(date);
|
|
93
|
+
return d >= cutoff && d < endDate;
|
|
94
|
+
})
|
|
95
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
96
|
+
.map(([date, data]) => ({
|
|
97
|
+
date,
|
|
98
|
+
cost: data.cost?.total ?? 0,
|
|
99
|
+
saved: data.cost?.saved ?? 0,
|
|
100
|
+
tokens: data.total ?? 0,
|
|
101
|
+
cached: data.cached ?? 0,
|
|
102
|
+
executions: data.executions ?? 0,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// Build parent node skeletons
|
|
106
|
+
const SKIP_KEYS = new Set(['version', 'lastUpdated', 'totals']);
|
|
107
|
+
const parentNodes = {};
|
|
108
|
+
for (const p of PARENT_CEREMONIES) {
|
|
109
|
+
parentNodes[p] = { name: p, calls: 0, tokens: 0, cost: 0, cached: 0, saved: 0, stages: [] };
|
|
110
|
+
}
|
|
111
|
+
const orphans = []; // keys that don't map to a known parent
|
|
112
|
+
|
|
113
|
+
for (const [key, value] of Object.entries(history)) {
|
|
114
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
115
|
+
if (!value || typeof value !== 'object') continue;
|
|
116
|
+
|
|
117
|
+
let totalInput = 0, totalOutput = 0, totalCost = 0, totalExec = 0, totalCached = 0, totalSaved = 0;
|
|
118
|
+
const dailyForKey = value.daily ?? {};
|
|
119
|
+
for (const [date, data] of Object.entries(dailyForKey)) {
|
|
120
|
+
const d = new Date(date);
|
|
121
|
+
if (d >= cutoff && d < endDate) {
|
|
122
|
+
totalInput += data.input ?? 0;
|
|
123
|
+
totalOutput += data.output ?? 0;
|
|
124
|
+
totalCost += data.cost?.total ?? 0;
|
|
125
|
+
totalExec += data.executions ?? 0;
|
|
126
|
+
totalCached += data.cached ?? 0;
|
|
127
|
+
totalSaved += data.cost?.saved ?? 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (totalExec === 0 && totalInput === 0 && totalOutput === 0) continue;
|
|
132
|
+
|
|
133
|
+
const entry = { name: key, calls: totalExec, tokens: totalInput + totalOutput, cost: totalCost, cached: totalCached, saved: totalSaved };
|
|
134
|
+
const parent = getParentCeremony(key);
|
|
135
|
+
|
|
136
|
+
if (parent && parentNodes[parent]) {
|
|
137
|
+
// Don't add a ceremony as a stage of itself — only add sub-stages
|
|
138
|
+
if (key !== parent) {
|
|
139
|
+
parentNodes[parent].stages.push(entry);
|
|
140
|
+
}
|
|
141
|
+
parentNodes[parent].calls += totalExec;
|
|
142
|
+
parentNodes[parent].tokens += totalInput + totalOutput;
|
|
143
|
+
parentNodes[parent].cost += totalCost;
|
|
144
|
+
parentNodes[parent].cached += totalCached;
|
|
145
|
+
parentNodes[parent].saved += totalSaved;
|
|
146
|
+
} else {
|
|
147
|
+
orphans.push({ ...entry, stages: [] });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Sort stages within each parent by cost desc
|
|
152
|
+
const ceremonies = [
|
|
153
|
+
...Object.values(parentNodes).filter(c => c.cost > 0 || c.tokens > 0),
|
|
154
|
+
...orphans,
|
|
155
|
+
]
|
|
156
|
+
.map(c => ({ ...c, stages: (c.stages || []).sort((a, b) => b.cost - a.cost) }))
|
|
157
|
+
.sort((a, b) => b.cost - a.cost);
|
|
158
|
+
|
|
159
|
+
res.json({ daily, ceremonies });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return router;
|
|
163
|
+
}
|