headlesscode 1.0.2
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/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `headlesscode trend` subcommand — a local, auto-refreshing, multi-repo
|
|
3
|
+
* cost-efficiency trend page. Thin dispatch to src/dashboard/trend.ts, same
|
|
4
|
+
* shape as dashboard/cli.ts.
|
|
5
|
+
*
|
|
6
|
+
* npx tsx src/cli.ts trend --repo <a> [--repo <b> ...] [--port 4460]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { startTrendServer } from "./trend.js"
|
|
10
|
+
|
|
11
|
+
const TREND_USAGE = `headlesscode trend — local, live-updating, multi-repo cost-efficiency trend
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
headlesscode trend --repo <path> [--repo <path> ...] [--port 4460]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--repo <path> Repo to include (repeatable). At least one required. Reads
|
|
18
|
+
that repo's central-store cost-history.jsonl /
|
|
19
|
+
session-cost-history.jsonl LIVE on every page load — no
|
|
20
|
+
separate build/index step, nothing to keep in sync.
|
|
21
|
+
--port <n> Port to listen on (default: 4460, or $HEADLESSCODE_TREND_PORT)
|
|
22
|
+
--help Show this help and exit
|
|
23
|
+
|
|
24
|
+
Binds to 127.0.0.1 only, read-only, no auth — matches the dashboard's
|
|
25
|
+
local-only contract.
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
interface TrendCliOptions {
|
|
29
|
+
repos: string[]
|
|
30
|
+
port: number
|
|
31
|
+
help: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseTrendArgs(argv: string[]): { options: TrendCliOptions; error?: string } {
|
|
35
|
+
const options: TrendCliOptions = {
|
|
36
|
+
repos: [],
|
|
37
|
+
port: Number(process.env.HEADLESSCODE_TREND_PORT ?? 4460),
|
|
38
|
+
help: false,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const arg = argv[i]
|
|
43
|
+
const eq = arg.indexOf("=")
|
|
44
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq)
|
|
45
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
|
|
46
|
+
const next = (): string | undefined => {
|
|
47
|
+
if (inlineValue !== undefined) {
|
|
48
|
+
return inlineValue
|
|
49
|
+
}
|
|
50
|
+
const v = argv[i + 1]
|
|
51
|
+
if (v === undefined || v.startsWith("--")) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
i++
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
switch (flag) {
|
|
59
|
+
case "--repo": {
|
|
60
|
+
const v = next()
|
|
61
|
+
if (v === undefined) {
|
|
62
|
+
return { options, error: "Missing value for --repo" }
|
|
63
|
+
}
|
|
64
|
+
options.repos.push(v)
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
case "--port": {
|
|
68
|
+
const v = next()
|
|
69
|
+
const n = v === undefined ? Number.NaN : Number(v)
|
|
70
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
71
|
+
return { options, error: "--port requires an integer between 1 and 65535" }
|
|
72
|
+
}
|
|
73
|
+
options.port = n
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
case "--help":
|
|
77
|
+
case "-h":
|
|
78
|
+
options.help = true
|
|
79
|
+
break
|
|
80
|
+
default:
|
|
81
|
+
return { options, error: `Unknown argument: ${arg}` }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { options }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function trendMain(argv: string[]): Promise<number> {
|
|
89
|
+
const { options, error } = parseTrendArgs(argv)
|
|
90
|
+
if (error) {
|
|
91
|
+
process.stderr.write(`headlesscode trend: ${error}\n\n${TREND_USAGE}`)
|
|
92
|
+
return 2
|
|
93
|
+
}
|
|
94
|
+
if (options.help) {
|
|
95
|
+
process.stdout.write(TREND_USAGE)
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
if (options.repos.length === 0) {
|
|
99
|
+
process.stderr.write(`headlesscode trend: at least one --repo is required\n\n${TREND_USAGE}`)
|
|
100
|
+
return 2
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let server: Awaited<ReturnType<typeof startTrendServer>>
|
|
104
|
+
try {
|
|
105
|
+
server = await startTrendServer({ port: options.port, repos: options.repos })
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
108
|
+
if ((err as NodeJS.ErrnoException)?.code === "EADDRINUSE") {
|
|
109
|
+
process.stderr.write(`headlesscode trend: port ${options.port} is already in use. Pick another with --port <n>.\n`)
|
|
110
|
+
return 2
|
|
111
|
+
}
|
|
112
|
+
process.stderr.write(`headlesscode trend: failed to start: ${message}\n`)
|
|
113
|
+
return 2
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
process.stdout.write(`[trend] listening on http://127.0.0.1:${options.port} (repos: ${options.repos.join(", ")})\n`)
|
|
117
|
+
|
|
118
|
+
return new Promise<number>((resolve) => {
|
|
119
|
+
let shuttingDown = false
|
|
120
|
+
const onSignal = (): void => {
|
|
121
|
+
// Only the first signal shuts down; a second Ctrl-C while the
|
|
122
|
+
// server is mid-close is ignored (the process exits on its own
|
|
123
|
+
// once the listening handle is gone).
|
|
124
|
+
if (shuttingDown) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
shuttingDown = true
|
|
128
|
+
process.stdout.write("\n[trend] shutting down\n")
|
|
129
|
+
process.removeListener("SIGINT", onSignal)
|
|
130
|
+
process.removeListener("SIGTERM", onSignal)
|
|
131
|
+
// Release the port: stop accepting new connections and drop
|
|
132
|
+
// keep-alive sockets so the process can exit. Without close the
|
|
133
|
+
// listening handle keeps the event loop alive and the port bound.
|
|
134
|
+
server.close()
|
|
135
|
+
server.closeAllConnections?.()
|
|
136
|
+
resolve(0)
|
|
137
|
+
}
|
|
138
|
+
process.on("SIGINT", onSignal)
|
|
139
|
+
process.on("SIGTERM", onSignal)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-repo cost-efficiency trend — `headlesscode trend --repo <a> --repo <b> ...`.
|
|
3
|
+
*
|
|
4
|
+
* The single-repo dashboard's "Cost history" section (issue #29) already
|
|
5
|
+
* charts one repo's rounds. This is a different question: comparing
|
|
6
|
+
* multiple repos side by side, and surfacing whether the harness is
|
|
7
|
+
* getting more or less efficient over time, computed FRESH from
|
|
8
|
+
* cost-history.jsonl / session-cost-history.jsonl on every request (no
|
|
9
|
+
* cached snapshot) so the page reflects live data as new rounds settle.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately data-driven, not hardcoded to specific issue numbers: the
|
|
12
|
+
* insight bullets below are generic computations (wasted-session
|
|
13
|
+
* clustering, cost/iteration vs. round-size correlation, rework rate,
|
|
14
|
+
* per-repo averages) that keep making sense as new rounds accumulate,
|
|
15
|
+
* rather than prose written for one particular investigation.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as http from "node:http"
|
|
19
|
+
import * as path from "node:path"
|
|
20
|
+
|
|
21
|
+
import { readCostHistory, readSessionCostHistory, type CostHistoryRecord, type SessionCostRecord } from "../orchestrator/cost-history.js"
|
|
22
|
+
|
|
23
|
+
export interface TrendPoint {
|
|
24
|
+
recordedAt: string
|
|
25
|
+
groupName: string
|
|
26
|
+
issues: number[]
|
|
27
|
+
costUsd: number
|
|
28
|
+
iterations: number
|
|
29
|
+
/** $ per 1,000 iterations — normalizes cheap and expensive rounds onto one scale. */
|
|
30
|
+
rate: number
|
|
31
|
+
continuationCount: number
|
|
32
|
+
reworkCount: number
|
|
33
|
+
/** True when a non-success session was recorded at the same settle event (see matchWastedSessions). */
|
|
34
|
+
wasted: boolean
|
|
35
|
+
wastedNote?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RepoTrend {
|
|
39
|
+
name: string
|
|
40
|
+
points: TrendPoint[]
|
|
41
|
+
avgRate: number
|
|
42
|
+
totalCost: number
|
|
43
|
+
wastedCost: number
|
|
44
|
+
wastedCount: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TrendData {
|
|
48
|
+
repos: RepoTrend[]
|
|
49
|
+
insights: string[]
|
|
50
|
+
generatedAt: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Pearson correlation coefficient; NaN-safe (returns 0 for degenerate input). */
|
|
54
|
+
function correlation(xs: number[], ys: number[]): number {
|
|
55
|
+
const n = xs.length
|
|
56
|
+
if (n < 2) {
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
const meanX = xs.reduce((s, v) => s + v, 0) / n
|
|
60
|
+
const meanY = ys.reduce((s, v) => s + v, 0) / n
|
|
61
|
+
let num = 0
|
|
62
|
+
let denX = 0
|
|
63
|
+
let denY = 0
|
|
64
|
+
for (let i = 0; i < n; i++) {
|
|
65
|
+
const dx = xs[i] - meanX
|
|
66
|
+
const dy = ys[i] - meanY
|
|
67
|
+
num += dx * dy
|
|
68
|
+
denX += dx * dx
|
|
69
|
+
denY += dy * dy
|
|
70
|
+
}
|
|
71
|
+
const den = Math.sqrt(denX * denY)
|
|
72
|
+
return den === 0 ? 0 : num / den
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Group and session records at the same settle event land within this window of each other. */
|
|
76
|
+
const SETTLE_EVENT_WINDOW_MS = 30_000
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A group record is "wasted-adjacent" when a non-success session was
|
|
80
|
+
* recorded at the SAME settle event. recordGroupCost + recordAllSessionCosts
|
|
81
|
+
* both flush at group-settle time (see cost-history.ts) but stamp their OWN
|
|
82
|
+
* `new Date().toISOString()` a few ms apart — never identical — so pairing
|
|
83
|
+
* requires a small tolerance window, not an exact string match. Each group
|
|
84
|
+
* record claims the closest same-groupName non-success session(s) within
|
|
85
|
+
* that window; a session matched to one group record is not reused for
|
|
86
|
+
* another.
|
|
87
|
+
*/
|
|
88
|
+
function buildTrendPoints(groups: CostHistoryRecord[], sessions: SessionCostRecord[]): TrendPoint[] {
|
|
89
|
+
const wastedSessions = sessions.filter((s) => s.status !== "success")
|
|
90
|
+
const claimed = new Set<SessionCostRecord>()
|
|
91
|
+
return [...groups]
|
|
92
|
+
.sort((a, b) => (a.recordedAt < b.recordedAt ? -1 : a.recordedAt > b.recordedAt ? 1 : 0))
|
|
93
|
+
.map((g) => {
|
|
94
|
+
const groupMs = Date.parse(g.recordedAt)
|
|
95
|
+
const wasted = wastedSessions.filter(
|
|
96
|
+
(s) =>
|
|
97
|
+
!claimed.has(s) &&
|
|
98
|
+
s.groupName === g.groupName &&
|
|
99
|
+
Math.abs(Date.parse(s.recordedAt) - groupMs) <= SETTLE_EVENT_WINDOW_MS,
|
|
100
|
+
)
|
|
101
|
+
wasted.forEach((s) => claimed.add(s))
|
|
102
|
+
return {
|
|
103
|
+
recordedAt: g.recordedAt,
|
|
104
|
+
groupName: g.groupName,
|
|
105
|
+
issues: g.issues,
|
|
106
|
+
costUsd: g.costUsd,
|
|
107
|
+
iterations: g.iterations,
|
|
108
|
+
rate: g.iterations > 0 ? (g.costUsd / g.iterations) * 1000 : 0,
|
|
109
|
+
continuationCount: g.continuationCount,
|
|
110
|
+
reworkCount: g.reworkCount,
|
|
111
|
+
wasted: wasted.length > 0,
|
|
112
|
+
wastedNote:
|
|
113
|
+
wasted.length > 0
|
|
114
|
+
? wasted.map((w) => `${w.mode} ${w.status} ($${w.costUsd.toFixed(4)}, ${w.iterations} iter)`).join("; ")
|
|
115
|
+
: undefined,
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function computeTrendData(repos: Array<{ name: string; groups: CostHistoryRecord[]; sessions: SessionCostRecord[] }>): TrendData {
|
|
121
|
+
const repoTrends: RepoTrend[] = repos.map(({ name, groups, sessions }) => {
|
|
122
|
+
const points = buildTrendPoints(groups, sessions)
|
|
123
|
+
const totalCost = points.reduce((s, p) => s + p.costUsd, 0)
|
|
124
|
+
const avgRate = points.length > 0 ? points.reduce((s, p) => s + p.rate, 0) / points.length : 0
|
|
125
|
+
const wastedSessions = sessions.filter((s) => s.status !== "success")
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
points,
|
|
129
|
+
avgRate,
|
|
130
|
+
totalCost,
|
|
131
|
+
wastedCost: wastedSessions.reduce((s, w) => s + w.costUsd, 0),
|
|
132
|
+
wastedCount: wastedSessions.length,
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const insights: string[] = []
|
|
137
|
+
|
|
138
|
+
// Wasted-session clustering: are they all in the past, or ongoing?
|
|
139
|
+
const allWasted = repoTrends.flatMap((r) =>
|
|
140
|
+
r.points.filter((p) => p.wasted).map((p) => ({ repo: r.name, ...p })),
|
|
141
|
+
)
|
|
142
|
+
if (allWasted.length > 0) {
|
|
143
|
+
const totalWastedCost = repoTrends.reduce((s, r) => s + r.wastedCost, 0)
|
|
144
|
+
const totalWastedCount = repoTrends.reduce((s, r) => s + r.wastedCount, 0)
|
|
145
|
+
const lastWastedAt = allWasted.reduce((max, p) => (p.recordedAt > max ? p.recordedAt : max), allWasted[0].recordedAt)
|
|
146
|
+
const roundsSince = repoTrends.reduce(
|
|
147
|
+
(s, r) => s + r.points.filter((p) => p.recordedAt > lastWastedAt).length,
|
|
148
|
+
0,
|
|
149
|
+
)
|
|
150
|
+
if (roundsSince > 0) {
|
|
151
|
+
insights.push(
|
|
152
|
+
`${totalWastedCount} wasted session(s) ($${totalWastedCost.toFixed(4)}) recorded, all before ${lastWastedAt} — zero wasted sessions in the ${roundsSince} round(s) recorded since. If that holds as more rounds land, whatever fix landed around then is working.`,
|
|
153
|
+
)
|
|
154
|
+
} else {
|
|
155
|
+
insights.push(
|
|
156
|
+
`${totalWastedCount} wasted session(s) ($${totalWastedCost.toFixed(4)}) recorded, most recently at ${lastWastedAt} — still an active source of waste; worth checking what those sessions have in common.`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
insights.push("No wasted sessions (session-level error/killed/budget outcomes) recorded in this data.")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Cost/iteration vs. round size: does a bigger round cost MORE per iteration, not just more iterations?
|
|
164
|
+
const allPoints = repoTrends.flatMap((r) => r.points)
|
|
165
|
+
if (allPoints.length >= 4) {
|
|
166
|
+
const corr = correlation(allPoints.map((p) => p.iterations), allPoints.map((p) => p.rate))
|
|
167
|
+
if (corr > 0.35) {
|
|
168
|
+
insights.push(
|
|
169
|
+
`Cost per iteration correlates positively with round size (r=${corr.toFixed(2)} across ${allPoints.length} rounds) — larger rounds aren't just longer, each iteration gets more expensive too. Splitting oversized issues before dispatch likely helps more than tuning iteration count alone.`,
|
|
170
|
+
)
|
|
171
|
+
} else if (corr < -0.35) {
|
|
172
|
+
insights.push(
|
|
173
|
+
`Cost per iteration correlates negatively with round size (r=${corr.toFixed(2)}) — larger rounds are actually getting cheaper per iteration, possibly from cache-hit growth outpacing complexity.`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Rework rate.
|
|
179
|
+
if (allPoints.length > 0) {
|
|
180
|
+
const reworked = allPoints.filter((p) => p.reworkCount > 0).length
|
|
181
|
+
insights.push(
|
|
182
|
+
`${reworked}/${allPoints.length} round(s) (${((reworked / allPoints.length) * 100).toFixed(0)}%) needed at least one rework cycle.`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Per-repo average comparison.
|
|
187
|
+
if (repoTrends.length >= 2) {
|
|
188
|
+
const sorted = [...repoTrends].filter((r) => r.points.length > 0).sort((a, b) => b.avgRate - a.avgRate)
|
|
189
|
+
if (sorted.length >= 2) {
|
|
190
|
+
const [highest, lowest] = [sorted[0], sorted[sorted.length - 1]]
|
|
191
|
+
if (highest.name !== lowest.name && lowest.avgRate > 0) {
|
|
192
|
+
const ratio = highest.avgRate / lowest.avgRate
|
|
193
|
+
insights.push(
|
|
194
|
+
`${highest.name} averages $${highest.avgRate.toFixed(3)}/1k-iter vs. ${lowest.name}'s $${lowest.avgRate.toFixed(3)}/1k-iter — ${ratio.toFixed(1)}x higher, consistent with ${highest.name} handling larger/less-familiar tasks.`,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { repos: repoTrends, insights, generatedAt: new Date().toISOString() }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface TrendServerOptions {
|
|
204
|
+
port: number
|
|
205
|
+
repos: string[]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function loadTrendData(repoPaths: string[]): Promise<TrendData> {
|
|
209
|
+
const repos = await Promise.all(
|
|
210
|
+
repoPaths.map(async (repoPath) => {
|
|
211
|
+
const [groups, sessions] = await Promise.all([readCostHistory(repoPath), readSessionCostHistory(repoPath)])
|
|
212
|
+
return { name: path.basename(repoPath.replace(/\/$/, "")), groups, sessions }
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
return computeTrendData(repos)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function startTrendServer(options: TrendServerOptions): Promise<http.Server> {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const server = http.createServer((req, res) => {
|
|
221
|
+
const url = new URL(req.url ?? "/", "http://localhost")
|
|
222
|
+
if (url.pathname === "/api/trend") {
|
|
223
|
+
loadTrendData(options.repos)
|
|
224
|
+
.then((data) => {
|
|
225
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" })
|
|
226
|
+
res.end(JSON.stringify(data))
|
|
227
|
+
})
|
|
228
|
+
.catch((err) => {
|
|
229
|
+
res.writeHead(500, { "content-type": "application/json; charset=utf-8" })
|
|
230
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
231
|
+
})
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
if (url.pathname === "/") {
|
|
235
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" })
|
|
236
|
+
res.end(renderTrendPage(options.repos))
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
res.writeHead(404, { "content-type": "text/plain" })
|
|
240
|
+
res.end("not found")
|
|
241
|
+
})
|
|
242
|
+
server.on("error", reject)
|
|
243
|
+
server.listen(options.port, "127.0.0.1", () => resolve(server))
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renderTrendPage(repoPaths: string[]): string {
|
|
248
|
+
return `<!doctype html>
|
|
249
|
+
<html>
|
|
250
|
+
<head>
|
|
251
|
+
<meta charset="utf-8">
|
|
252
|
+
<title>headlesscode: cost-efficiency trend</title>
|
|
253
|
+
<style>
|
|
254
|
+
:root {
|
|
255
|
+
color-scheme: light;
|
|
256
|
+
--surface-1: #fcfcfb; --page: #f9f9f7; --text-primary: #0b0b0b; --text-secondary: #52514e;
|
|
257
|
+
--text-muted: #898781; --grid: #e1e0d9; --baseline: #c3c2b7; --border: rgba(11,11,11,0.10);
|
|
258
|
+
--series-1: #2a78d6; --series-2: #eb6834; --series-3: #1baf7a; --series-4: #eda100;
|
|
259
|
+
--status-critical: #d03b3b; --status-good: #0ca30c;
|
|
260
|
+
}
|
|
261
|
+
@media (prefers-color-scheme: dark) {
|
|
262
|
+
:root {
|
|
263
|
+
color-scheme: dark;
|
|
264
|
+
--surface-1: #1a1a19; --page: #0d0d0d; --text-primary: #ffffff; --text-secondary: #c3c2b7;
|
|
265
|
+
--text-muted: #898781; --grid: #2c2c2a; --baseline: #383835; --border: rgba(255,255,255,0.10);
|
|
266
|
+
--series-1: #3987e5; --series-2: #d95926; --series-3: #199e70; --series-4: #c98500;
|
|
267
|
+
--status-critical: #e66767; --status-good: #0ca30c;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
body { background: var(--page); color: var(--text-primary); margin: 0; font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; }
|
|
271
|
+
.wrap { max-width: 980px; margin: 0 auto; padding: 32px 20px 64px; }
|
|
272
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
273
|
+
.sub { color: var(--text-secondary); font-size: 14px; margin: 0 0 8px; }
|
|
274
|
+
.stamp { color: var(--text-muted); font-size: 12px; margin: 0 0 28px; }
|
|
275
|
+
.card { background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px; padding: 20px; margin-bottom: 20px; }
|
|
276
|
+
.card h2 { font-size: 15px; margin: 0 0 12px; }
|
|
277
|
+
svg { display: block; overflow: visible; }
|
|
278
|
+
.axis-label { fill: var(--text-muted); font-size: 10px; }
|
|
279
|
+
.gridline { stroke: var(--grid); stroke-width: 1; }
|
|
280
|
+
.baseline { stroke: var(--baseline); stroke-width: 1; }
|
|
281
|
+
.legend { display: flex; gap: 16px; font-size: 12px; color: var(--text-secondary); margin: 10px 0 2px; flex-wrap: wrap; }
|
|
282
|
+
.legend .dot { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }
|
|
283
|
+
.tooltip { position: fixed; pointer-events: none; z-index: 50; background: var(--text-primary); color: var(--surface-1);
|
|
284
|
+
font-size: 12px; padding: 6px 9px; border-radius: 6px; line-height: 1.4; opacity: 0; transition: opacity 80ms; max-width: 260px; }
|
|
285
|
+
ul.insights { margin: 0; padding-left: 20px; font-size: 14px; line-height: 1.6; }
|
|
286
|
+
ul.insights li { margin-bottom: 10px; }
|
|
287
|
+
.refresh-note { color: var(--text-muted); font-size: 12px; }
|
|
288
|
+
</style>
|
|
289
|
+
</head>
|
|
290
|
+
<body>
|
|
291
|
+
<div class="wrap">
|
|
292
|
+
<h1>headlesscode: cost-efficiency trend</h1>
|
|
293
|
+
<p class="sub">Live from <code>cost-history.jsonl</code> across ${repoPaths.length} repo(s): ${repoPaths.map((p) => path.basename(p)).join(", ")}. Refreshes every 30s.</p>
|
|
294
|
+
<p class="stamp" id="stamp">loading…</p>
|
|
295
|
+
|
|
296
|
+
<div class="card">
|
|
297
|
+
<h2>Cost per 1,000 iterations, chronological (all repos, own timeline each)</h2>
|
|
298
|
+
<div id="chart-lines"></div>
|
|
299
|
+
<div class="legend" id="legend-lines"></div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div class="card">
|
|
303
|
+
<h2>Repo averages</h2>
|
|
304
|
+
<div id="chart-bars"></div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="card">
|
|
308
|
+
<h2>What the data says</h2>
|
|
309
|
+
<ul class="insights" id="insights"></ul>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="tooltip" id="tooltip"></div>
|
|
313
|
+
<script>
|
|
314
|
+
const SERIES = ["var(--series-1)","var(--series-2)","var(--series-3)","var(--series-4)"];
|
|
315
|
+
const tooltip = document.getElementById("tooltip");
|
|
316
|
+
function showTip(evt, html) {
|
|
317
|
+
tooltip.innerHTML = html;
|
|
318
|
+
tooltip.style.opacity = 1;
|
|
319
|
+
tooltip.style.left = (evt.clientX + 14) + "px";
|
|
320
|
+
tooltip.style.top = (evt.clientY + 14) + "px";
|
|
321
|
+
}
|
|
322
|
+
function hideTip() { tooltip.style.opacity = 0; }
|
|
323
|
+
window.__tip = showTip;
|
|
324
|
+
window.__hidetip = hideTip;
|
|
325
|
+
|
|
326
|
+
function lineChart(el, legendEl, repos) {
|
|
327
|
+
const W = 900, H = 240, padL = 44, padR = 12, padT = 14, padB = 26;
|
|
328
|
+
const plotW = W - padL - padR, plotH = H - padT - padB;
|
|
329
|
+
const allRates = repos.flatMap(r => r.points.map(p => p.rate));
|
|
330
|
+
const maxY = (Math.max(1e-6, ...allRates)) * 1.15;
|
|
331
|
+
const maxLen = Math.max(1, ...repos.map(r => r.points.length));
|
|
332
|
+
const x = i => padL + (maxLen <= 1 ? 0 : (i / (maxLen - 1)) * plotW);
|
|
333
|
+
const y = v => padT + plotH - (v / maxY) * plotH;
|
|
334
|
+
|
|
335
|
+
let gridlines = "";
|
|
336
|
+
for (let s = 0; s <= 4; s++) {
|
|
337
|
+
const val = (maxY/4)*s, yy = y(val);
|
|
338
|
+
gridlines += \`<line class="gridline" x1="\${padL}" x2="\${W-padR}" y1="\${yy}" y2="\${yy}"/>
|
|
339
|
+
<text class="axis-label" x="\${padL-6}" y="\${yy+3}" text-anchor="end">\${val.toFixed(2)}</text>\`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let body = "";
|
|
343
|
+
repos.forEach((r, ri) => {
|
|
344
|
+
const color = SERIES[ri % SERIES.length];
|
|
345
|
+
if (r.points.length === 0) return;
|
|
346
|
+
const pathD = r.points.map((p,i) => \`\${i===0?"M":"L"}\${x(i).toFixed(1)},\${y(p.rate).toFixed(1)}\`).join(" ");
|
|
347
|
+
body += \`<path d="\${pathD}" fill="none" stroke="\${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>\`;
|
|
348
|
+
body += r.points.map((p,i) => {
|
|
349
|
+
const cx = x(i), cy = y(p.rate);
|
|
350
|
+
const rad = p.wasted ? 5 : 3.5;
|
|
351
|
+
const fill = p.wasted ? "var(--status-critical)" : color;
|
|
352
|
+
const stroke = p.wasted ? \`stroke="var(--surface-1)" stroke-width="2"\` : "";
|
|
353
|
+
const tip = \`<b>\${r.name} · \${p.groupName} · #\${p.issues.join(",")}</b><br>$\${p.costUsd.toFixed(4)} · \${p.iterations} iter · \${p.rate.toFixed(3)} $/1k-iter\${p.wasted ? "<br>wasted: " + p.wastedNote : ""}\`;
|
|
354
|
+
return \`<circle cx="\${cx.toFixed(1)}" cy="\${cy.toFixed(1)}" r="\${rad}" fill="\${fill}" \${stroke}
|
|
355
|
+
onmousemove='window.__tip(event, \${JSON.stringify(tip)})' onmouseleave='window.__hidetip()' style="cursor:pointer"/>\`;
|
|
356
|
+
}).join("");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
el.innerHTML = \`<svg viewBox="0 0 \${W} \${H}" width="100%" height="\${H}">
|
|
360
|
+
\${gridlines}
|
|
361
|
+
<line class="baseline" x1="\${padL}" x2="\${W-padR}" y1="\${padT+plotH}" y2="\${padT+plotH}"/>
|
|
362
|
+
\${body}
|
|
363
|
+
</svg>\`;
|
|
364
|
+
|
|
365
|
+
legendEl.innerHTML = repos.map((r, ri) => \`<span><span class="dot" style="background:\${SERIES[ri % SERIES.length]}"></span>\${r.name}</span>\`).join("")
|
|
366
|
+
+ \`<span><span class="dot" style="background:var(--status-critical)"></span>wasted session</span>\`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function barChart(el, repos) {
|
|
370
|
+
const W = 500, H = 180, padL = 44, padR = 12, padT = 14, padB = 30;
|
|
371
|
+
const plotW = W - padL - padR, plotH = H - padT - padB;
|
|
372
|
+
const maxY = Math.max(1e-6, ...repos.map(r => r.avgRate)) * 1.2;
|
|
373
|
+
const y = v => padT + plotH - (v / maxY) * plotH;
|
|
374
|
+
const bw = plotW / Math.max(1, repos.length) * 0.4;
|
|
375
|
+
let gridlines = "";
|
|
376
|
+
for (let s = 0; s <= 4; s++) {
|
|
377
|
+
const val = (maxY/4)*s, yy = y(val);
|
|
378
|
+
gridlines += \`<line class="gridline" x1="\${padL}" x2="\${W-padR}" y1="\${yy}" y2="\${yy}"/>
|
|
379
|
+
<text class="axis-label" x="\${padL-6}" y="\${yy+3}" text-anchor="end">\${val.toFixed(2)}</text>\`;
|
|
380
|
+
}
|
|
381
|
+
const bars = repos.map((r,i) => {
|
|
382
|
+
const cx = padL + (i+0.5)/repos.length*plotW;
|
|
383
|
+
const barY = y(r.avgRate), barH = (padT+plotH)-barY;
|
|
384
|
+
const color = SERIES[i % SERIES.length];
|
|
385
|
+
return \`<rect x="\${(cx-bw/2).toFixed(1)}" y="\${barY.toFixed(1)}" width="\${bw.toFixed(1)}" height="\${barH.toFixed(1)}" rx="4" fill="\${color}"/>
|
|
386
|
+
<text class="axis-label" x="\${cx.toFixed(1)}" y="\${padT+plotH+16}" text-anchor="middle">\${r.name}</text>
|
|
387
|
+
<text x="\${cx.toFixed(1)}" y="\${(barY-6).toFixed(1)}" text-anchor="middle" font-size="12" fill="var(--text-primary)" font-weight="600">\${r.avgRate.toFixed(3)}</text>\`;
|
|
388
|
+
}).join("");
|
|
389
|
+
el.innerHTML = \`<svg viewBox="0 0 \${W} \${H}" width="100%" height="\${H}">
|
|
390
|
+
\${gridlines}
|
|
391
|
+
<line class="baseline" x1="\${padL}" x2="\${W-padR}" y1="\${padT+plotH}" y2="\${padT+plotH}"/>
|
|
392
|
+
\${bars}
|
|
393
|
+
</svg>\`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function refresh() {
|
|
397
|
+
try {
|
|
398
|
+
const res = await fetch("/api/trend");
|
|
399
|
+
const data = await res.json();
|
|
400
|
+
document.getElementById("stamp").textContent = "last updated " + new Date(data.generatedAt).toLocaleString();
|
|
401
|
+
lineChart(document.getElementById("chart-lines"), document.getElementById("legend-lines"), data.repos);
|
|
402
|
+
barChart(document.getElementById("chart-bars"), data.repos);
|
|
403
|
+
document.getElementById("insights").innerHTML = data.insights.map(t => "<li>" + t + "</li>").join("");
|
|
404
|
+
} catch (err) {
|
|
405
|
+
document.getElementById("stamp").textContent = "failed to load: " + err;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
refresh();
|
|
409
|
+
setInterval(refresh, 30000);
|
|
410
|
+
</script>
|
|
411
|
+
</body>
|
|
412
|
+
</html>`
|
|
413
|
+
}
|