smlflg commited on
Commit
a873849
·
verified ·
1 Parent(s): 4ea7a7f

Initial public upload from Projekte/CloudCodeUsageAnalysis

Browse files
Files changed (7) hide show
  1. .codex/config.toml +31 -0
  2. README.md +43 -0
  3. app.js +261 -0
  4. data/analysis.json +0 -0
  5. index.html +115 -0
  6. scripts/build_analysis.py +577 -0
  7. styles.css +431 -0
.codex/config.toml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Coding MCPs: project-local registration (2026-09-07).
2
+
3
+ [mcp_servers.serena]
4
+ command = "/home/smlflg/.local/share/coding-mcp/serena/.venv/bin/serena"
5
+ args = ["start-mcp-server", "--context=codex", "--project", "/home/smlflg/Projekte/CloudCodeUsageAnalysis", "--enable-web-dashboard", "false", "--open-web-dashboard", "false", "--enable-gui-log-window", "false"]
6
+ cwd = "/home/smlflg/Projekte/CloudCodeUsageAnalysis"
7
+ enabled = true
8
+ startup_timeout_sec = 120
9
+ tool_timeout_sec = 180
10
+
11
+ [mcp_servers.serena.env]
12
+ "SERENA_HOME" = "/home/smlflg/.local/share/coding-mcp/serena/state"
13
+
14
+ [mcp_servers.gitnexus]
15
+ command = "/home/smlflg/.nvm/versions/node/v24.13.0/bin/node"
16
+ args = ["/home/smlflg/.local/share/coding-mcp/gitnexus/node_modules/gitnexus/dist/cli/index.js", "mcp"]
17
+ cwd = "/home/smlflg/Projekte/CloudCodeUsageAnalysis"
18
+ enabled = true
19
+ startup_timeout_sec = 120
20
+ tool_timeout_sec = 180
21
+
22
+ [mcp_servers.gitnexus.env]
23
+ "GITNEXUS_HOME" = "/home/smlflg/.local/share/coding-mcp/gitnexus/runtime"
24
+
25
+ [mcp_servers.context7]
26
+ command = "/home/smlflg/.nvm/versions/node/v24.13.0/bin/node"
27
+ args = ["/home/smlflg/.local/share/coding-mcp/context7/node_modules/@upstash/context7-mcp/dist/index.js"]
28
+ cwd = "/home/smlflg/Projekte/CloudCodeUsageAnalysis"
29
+ enabled = true
30
+ startup_timeout_sec = 120
31
+ tool_timeout_sec = 180
README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CloudCode Usage Analysis
2
+
3
+ Lokale Single-Page-Analyse deines Cloud-Code-Monats auf Basis der Projekte in `~/Projekte`.
4
+
5
+ ## Inhalt
6
+
7
+ - Executive Summary
8
+ - Arbeitszeit-Schaetzung mit Bandbreite
9
+ - Arbeitsprozess- und Workflow-Muster
10
+ - Projekt-Cluster und Aktivitaetsbild
11
+ - Ranking fuer das wertvollste Weiterentwicklungsprojekt
12
+
13
+ ## Generieren
14
+
15
+ ```bash
16
+ cd ~/ProjekteV2/CodexWebsiteSamuelClaudecode/CloudCodeUsageAnalysis
17
+ python3 scripts/build_analysis.py
18
+ ```
19
+
20
+ Das Script scannt `~/Projekte`, ignoriert fruehere/lower Sidecar-Versionen in der Hauptanalyse und schreibt `data/analysis.json`.
21
+
22
+ ## Lokal starten
23
+
24
+ ```bash
25
+ cd ~/ProjekteV2/CodexWebsiteSamuelClaudecode/CloudCodeUsageAnalysis
26
+ python3 -m http.server 8033
27
+ ```
28
+
29
+ Dann im Browser:
30
+
31
+ `http://localhost:8033`
32
+
33
+ ## Struktur
34
+
35
+ - `index.html` – Seite
36
+ - `styles.css` – visuelle Gestaltung
37
+ - `app.js` – Rendering der Analyse aus JSON
38
+ - `scripts/build_analysis.py` – Datenerhebung und Ableitung
39
+ - `data/analysis.json` – generierte Datenbasis
40
+
41
+ ## Hinweis
42
+
43
+ Diese Analyse veraendert keine bestehenden Projekte. Sie liest nur Git-Historie, Projektstruktur und Doku-Artefakte aus und erzeugt einen neuen separaten Analyse-Ordner.
app.js ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const formatHours = (value) => `${Number(value).toFixed(1)} h`;
2
+
3
+ const create = (tag, className, text) => {
4
+ const node = document.createElement(tag);
5
+ if (className) node.className = className;
6
+ if (text !== undefined) node.textContent = text;
7
+ return node;
8
+ };
9
+
10
+ const load = async () => {
11
+ const response = await fetch("./data/analysis.json");
12
+ if (!response.ok) throw new Error("analysis.json konnte nicht geladen werden");
13
+ return response.json();
14
+ };
15
+
16
+ const renderHero = (data) => {
17
+ document.querySelector("#headline").textContent = data.executive_summary.headline;
18
+ const panel = document.querySelector("#heroPanel");
19
+ const stats = [
20
+ ["Best Estimate", formatHours(data.work_estimate.best_estimate_hours)],
21
+ ["Aktive Projekte", String(data.work_estimate.active_project_count)],
22
+ ["Top Empfehlung", data.rankings.top_recommendation.name],
23
+ ["Analysefenster", `${data.meta.window_start} bis ${data.meta.window_end}`],
24
+ ];
25
+ stats.forEach(([label, value]) => {
26
+ const row = create("div", "summary-stat");
27
+ row.append(create("span", "", label));
28
+ row.append(create("strong", "", value));
29
+ panel.append(row);
30
+ });
31
+ };
32
+
33
+ const renderFindings = (data) => {
34
+ const host = document.querySelector("#findingCards");
35
+ data.executive_summary.key_findings.forEach((finding, index) => {
36
+ const card = create("article", "card");
37
+ card.append(create("div", "label", `Antwort ${index + 1}`));
38
+ card.append(create("p", "supporting", finding));
39
+ host.append(card);
40
+ });
41
+ };
42
+
43
+ const renderWork = (data) => {
44
+ const host = document.querySelector("#workCards");
45
+ const cards = [
46
+ ["Best Estimate", formatHours(data.work_estimate.best_estimate_hours), "Zentralwert aus Session-Bloecken, Commit-Spans und Artefakt-Bonus."],
47
+ ["Untergrenze", formatHours(data.work_estimate.lower_bound_hours), "Konservative Sicht: nur ein Teil der Session-Zeit zaehlt als aktive Build-Zeit."],
48
+ ["Obergrenze", formatHours(data.work_estimate.upper_bound_hours), "Erweitert um stille Vor-/Nacharbeit, verworfene Versuche und uncommitted Arbeit."],
49
+ ];
50
+ cards.forEach(([label, metric, support]) => {
51
+ const card = create("article", "card");
52
+ card.append(create("div", "label", label));
53
+ card.append(create("p", "metric", metric));
54
+ card.append(create("p", "supporting", support));
55
+ host.append(card);
56
+ });
57
+
58
+ const methodList = document.querySelector("#methodList");
59
+ data.work_estimate.method.forEach((line) => methodList.append(create("li", "", line)));
60
+ const uncertaintyList = document.querySelector("#uncertaintyList");
61
+ data.work_estimate.uncertainties.forEach((line) => uncertaintyList.append(create("li", "", line)));
62
+ };
63
+
64
+ const renderWorkflow = (data) => {
65
+ const workflow = data.workflow_patterns;
66
+ const narrative = document.querySelector("#workflowNarrative");
67
+ [workflow.startup_pattern, workflow.iteration_pattern, workflow.parallel_vs_serial.evidence].forEach((line) => {
68
+ narrative.append(create("p", "", line));
69
+ });
70
+
71
+ const total = workflow.prototyping_vs_refinement.prototype_signals + workflow.prototyping_vs_refinement.refinement_signals;
72
+ const protoPct = total ? (workflow.prototyping_vs_refinement.prototype_signals / total) * 100 : 0;
73
+ const refinePct = 100 - protoPct;
74
+ const chart = document.querySelector("#ratioChart");
75
+ [
76
+ ["Prototyping", protoPct, "proto"],
77
+ ["Refinement", refinePct, "refine"],
78
+ ].forEach(([label, pct, klass]) => {
79
+ const row = create("div", "mini-row");
80
+ row.append(create("div", "", label));
81
+ const track = create("div", "track");
82
+ const fill = create("span");
83
+ fill.className = klass;
84
+ fill.style.width = `${pct}%`;
85
+ track.append(fill);
86
+ row.append(track);
87
+ row.append(create("div", "muted", `${pct.toFixed(0)}%`));
88
+ chart.append(row);
89
+ });
90
+ document.querySelector("#ratioNote").textContent = workflow.prototyping_vs_refinement.interpretation;
91
+
92
+ const cards = [
93
+ ["Seriell vs. Parallel", workflow.parallel_vs_serial.label],
94
+ ["Eval-Loops", workflow.evaluation_loops.interpretation],
95
+ ["Bevorzugte Projektarten", workflow.preferred_project_types.join(" • ")],
96
+ ["Meta-Systeme", workflow.meta_systems.join(" • ")],
97
+ ];
98
+ const host = document.querySelector("#workflowCards");
99
+ cards.forEach(([label, support]) => {
100
+ const card = create("article", "card");
101
+ card.append(create("div", "label", label));
102
+ card.append(create("p", "supporting", support));
103
+ host.append(card);
104
+ });
105
+ };
106
+
107
+ const renderClusters = (data) => {
108
+ const grid = document.querySelector("#clusterGrid");
109
+ data.project_clusters.forEach((cluster) => {
110
+ const card = create("article", "cluster-card");
111
+ card.append(create("div", "label", `${cluster.count} Projekte`));
112
+ card.append(create("h3", "", cluster.name));
113
+ card.append(create("p", "supporting", cluster.summary));
114
+ const pills = create("div", "pill-row");
115
+ cluster.projects.forEach((project) => {
116
+ pills.append(create("span", "pill", `${project.name} · ${project.commit_count}`));
117
+ });
118
+ card.append(pills);
119
+ grid.append(card);
120
+ });
121
+
122
+ const tableHost = document.querySelector("#projectTable");
123
+ const table = create("table");
124
+ table.innerHTML = `
125
+ <thead>
126
+ <tr>
127
+ <th>Projekt</th>
128
+ <th>Cluster</th>
129
+ <th>Commits</th>
130
+ <th>Sessions</th>
131
+ <th>Status</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody></tbody>
135
+ `;
136
+ const tbody = table.querySelector("tbody");
137
+ data.projects.slice(0, 12).forEach((project) => {
138
+ const row = document.createElement("tr");
139
+ row.innerHTML = `
140
+ <td>${project.name}</td>
141
+ <td>${project.cluster}</td>
142
+ <td>${project.commit_count}</td>
143
+ <td>${project.session_count}</td>
144
+ <td>${project.status}</td>
145
+ `;
146
+ tbody.append(row);
147
+ });
148
+ tableHost.append(table);
149
+ };
150
+
151
+ const renderTimeline = (data) => {
152
+ const host = document.querySelector("#timeline");
153
+ const grid = create("div", "timeline-grid");
154
+ data.timeline.forEach((day) => {
155
+ const cell = create("article", `day-cell level-${day.intensity}`);
156
+ const date = new Date(day.date);
157
+ const label = date.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" });
158
+ const header = create("div", "day-head");
159
+ header.append(create("span", "", label));
160
+ header.append(create("span", "", `${day.projects.length} Projekte`));
161
+ cell.append(header);
162
+ const projects = create("div", "day-projects");
163
+ if (day.projects.length === 0) {
164
+ projects.append(create("span", "muted", "ruhiger Tag"));
165
+ } else {
166
+ day.projects.slice(0, 3).forEach((project) => {
167
+ projects.append(create("span", "", `${project.name} · ${project.session_count} Session`));
168
+ });
169
+ }
170
+ cell.append(projects);
171
+ grid.append(cell);
172
+ });
173
+ host.append(grid);
174
+ };
175
+
176
+ const renderRanking = (data) => {
177
+ const top = data.rankings.top_recommendation;
178
+ const main = document.querySelector("#topRecommendation");
179
+ main.append(create("div", "badge", "Top Empfehlung"));
180
+ main.append(create("h3", "", top.name));
181
+ main.append(create("p", "metric", `${top.score.toFixed(2)} / 5`));
182
+ main.append(create("p", "supporting", top.summary));
183
+
184
+ const stack = create("div", "mini-stack");
185
+ Object.entries(top.scores).forEach(([label, value]) => {
186
+ const row = create("div", "mini-row");
187
+ row.append(create("div", "", label.replace("_", " ")));
188
+ const track = create("div", "track");
189
+ const fill = create("span");
190
+ fill.style.width = `${(value / 5) * 100}%`;
191
+ track.append(fill);
192
+ row.append(track);
193
+ row.append(create("div", "muted", value.toFixed(1)));
194
+ stack.append(row);
195
+ });
196
+ main.append(stack);
197
+
198
+ const topThree = document.querySelector("#topThree");
199
+ data.rankings.top_three.forEach((project, index) => {
200
+ const item = create("div", "rank-item");
201
+ item.innerHTML = `<strong>#${index + 1} ${project.name}</strong><p class="supporting">${project.summary}</p>`;
202
+ topThree.append(item);
203
+ });
204
+
205
+ const rankingTable = document.querySelector("#rankingTable");
206
+ const table = create("table");
207
+ table.innerHTML = `
208
+ <thead>
209
+ <tr>
210
+ <th>Projekt</th>
211
+ <th>Score</th>
212
+ <th>Commits</th>
213
+ <th>Kernaussage</th>
214
+ </tr>
215
+ </thead>
216
+ <tbody></tbody>
217
+ `;
218
+ const tbody = table.querySelector("tbody");
219
+ data.rankings.shortlist.forEach((project) => {
220
+ const row = document.createElement("tr");
221
+ row.innerHTML = `
222
+ <td>${project.name}</td>
223
+ <td>${project.score.toFixed(2)}</td>
224
+ <td>${project.commit_count}</td>
225
+ <td>${project.summary}</td>
226
+ `;
227
+ tbody.append(row);
228
+ });
229
+ rankingTable.append(table);
230
+ };
231
+
232
+ const renderEvidence = (data) => {
233
+ const host = document.querySelector("#evidenceGrid");
234
+ data.evidence.forEach((entry) => {
235
+ const card = create("article", "evidence-card");
236
+ card.append(create("div", "label", entry.project));
237
+ card.append(create("p", "supporting", `${entry.commits} Commits im Fenster · letzter Commit ${entry.last_commit ?? "n/a"}`));
238
+ const pills = create("div", "pill-row");
239
+ Object.entries(entry.artifacts).forEach(([key, value]) => pills.append(create("span", "pill", `${key}: ${value}`)));
240
+ card.append(pills);
241
+ const list = create("ul", "bullet-list");
242
+ entry.sample_messages.forEach((message) => list.append(create("li", "", message)));
243
+ card.append(list);
244
+ host.append(card);
245
+ });
246
+ };
247
+
248
+ load()
249
+ .then((data) => {
250
+ renderHero(data);
251
+ renderFindings(data);
252
+ renderWork(data);
253
+ renderWorkflow(data);
254
+ renderClusters(data);
255
+ renderTimeline(data);
256
+ renderRanking(data);
257
+ renderEvidence(data);
258
+ })
259
+ .catch((error) => {
260
+ document.body.innerHTML = `<main class="page-shell"><article class="panel"><h1>Ladefehler</h1><p>${error.message}</p></article></main>`;
261
+ });
data/analysis.json ADDED
The diff for this file is too large to render. See raw diff
 
index.html ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="de">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Cloud Code Usage Analysis</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="page-shell">
11
+ <header class="hero">
12
+ <div class="hero-copy">
13
+ <p class="eyebrow">Cloud Code Month Analysis</p>
14
+ <h1>Wie dein Monat mit Cloud Code aussah, in Evidenz statt Bauchgefuehl.</h1>
15
+ <p class="lede" id="headline"></p>
16
+ </div>
17
+ <aside class="hero-panel" id="heroPanel"></aside>
18
+ </header>
19
+
20
+ <main>
21
+ <section class="section" id="summarySection">
22
+ <div class="section-heading">
23
+ <p class="kicker">Executive Summary</p>
24
+ <h2>Drei klare Antworten</h2>
25
+ </div>
26
+ <div class="cards three" id="findingCards"></div>
27
+ </section>
28
+
29
+ <section class="section">
30
+ <div class="section-heading">
31
+ <p class="kicker">Arbeitszeit</p>
32
+ <h2>Geschaetzte investierte Stunden</h2>
33
+ </div>
34
+ <div class="cards three" id="workCards"></div>
35
+ <div class="split">
36
+ <article class="panel">
37
+ <h3>Methodik</h3>
38
+ <ul class="bullet-list" id="methodList"></ul>
39
+ </article>
40
+ <article class="panel">
41
+ <h3>Unsicherheiten</h3>
42
+ <ul class="bullet-list" id="uncertaintyList"></ul>
43
+ </article>
44
+ </div>
45
+ </section>
46
+
47
+ <section class="section">
48
+ <div class="section-heading">
49
+ <p class="kicker">Arbeitsprozess</p>
50
+ <h2>Wie du mit Cloud Code gearbeitet hast</h2>
51
+ </div>
52
+ <div class="split process-layout">
53
+ <article class="panel narrative" id="workflowNarrative"></article>
54
+ <article class="panel">
55
+ <h3>Protoyping vs. Refinement</h3>
56
+ <div id="ratioChart" class="ratio-chart"></div>
57
+ <div class="subnote" id="ratioNote"></div>
58
+ </article>
59
+ </div>
60
+ <div class="cards four" id="workflowCards"></div>
61
+ </section>
62
+
63
+ <section class="section">
64
+ <div class="section-heading">
65
+ <p class="kicker">Projektlandschaft</p>
66
+ <h2>Cluster und aktive Schwergewichte</h2>
67
+ </div>
68
+ <div class="cluster-grid" id="clusterGrid"></div>
69
+ <div class="panel">
70
+ <h3>Aktive Projekte</h3>
71
+ <div id="projectTable"></div>
72
+ </div>
73
+ </section>
74
+
75
+ <section class="section">
76
+ <div class="section-heading">
77
+ <p class="kicker">Timeline</p>
78
+ <h2>Aktivitaetsbild der letzten 30 Tage</h2>
79
+ </div>
80
+ <article class="panel">
81
+ <div id="timeline"></div>
82
+ </article>
83
+ </section>
84
+
85
+ <section class="section">
86
+ <div class="section-heading">
87
+ <p class="kicker">Strategische Empfehlung</p>
88
+ <h2>Welches Projekt jetzt den groessten Mehrwert hat</h2>
89
+ </div>
90
+ <div class="split">
91
+ <article class="panel recommendation-main" id="topRecommendation"></article>
92
+ <article class="panel">
93
+ <h3>Top 3</h3>
94
+ <div id="topThree"></div>
95
+ </article>
96
+ </div>
97
+ <article class="panel">
98
+ <h3>Shortlist</h3>
99
+ <div id="rankingTable"></div>
100
+ </article>
101
+ </section>
102
+
103
+ <section class="section">
104
+ <div class="section-heading">
105
+ <p class="kicker">Evidenz</p>
106
+ <h2>Worauf die Aussagen beruhen</h2>
107
+ </div>
108
+ <div class="evidence-grid" id="evidenceGrid"></div>
109
+ </section>
110
+ </main>
111
+ </div>
112
+
113
+ <script src="./app.js" type="module"></script>
114
+ </body>
115
+ </html>
scripts/build_analysis.py ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import math
6
+ import os
7
+ import subprocess
8
+ from collections import Counter, defaultdict
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timedelta
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ ROOT = Path("/home/smlflg/Projekte")
16
+ OUT = Path(__file__).resolve().parents[1] / "data" / "analysis.json"
17
+ WINDOW_END = datetime.fromisoformat("2026-03-20T23:59:59+01:00")
18
+ WINDOW_START = datetime.fromisoformat("2026-02-19T00:00:00+01:00")
19
+ SESSION_GAP_MINUTES = 120
20
+ IGNORED_PROJECTS = {
21
+ "Sidecar",
22
+ "Sidecar-V5",
23
+ "Sidecar-V6",
24
+ "Sidecar-V7-Lite",
25
+ "Sidecar-V7-V2",
26
+ "Sidecar-V8-Plan",
27
+ }
28
+ TOP_RECOMMENDATION_ORDER = [
29
+ "Sidecar-ng",
30
+ "MyAIGame",
31
+ "Sidecar-V7",
32
+ "ClaudeCodePanel",
33
+ "Sidecar-Evolution",
34
+ "AgentArena",
35
+ ]
36
+ RANKING_SCORES = {
37
+ "Sidecar-ng": {
38
+ "leverage": 5.0,
39
+ "reusability": 4.6,
40
+ "strategic_value": 5.0,
41
+ "product_readiness": 4.5,
42
+ "synergy": 5.0,
43
+ "workflow_fit": 5.0,
44
+ },
45
+ "MyAIGame": {
46
+ "leverage": 4.4,
47
+ "reusability": 5.0,
48
+ "strategic_value": 4.4,
49
+ "product_readiness": 3.4,
50
+ "synergy": 4.8,
51
+ "workflow_fit": 4.9,
52
+ },
53
+ "Sidecar-V7": {
54
+ "leverage": 4.3,
55
+ "reusability": 4.3,
56
+ "strategic_value": 4.4,
57
+ "product_readiness": 4.5,
58
+ "synergy": 4.3,
59
+ "workflow_fit": 4.4,
60
+ },
61
+ "ClaudeCodePanel": {
62
+ "leverage": 4.0,
63
+ "reusability": 3.4,
64
+ "strategic_value": 4.2,
65
+ "product_readiness": 4.8,
66
+ "synergy": 4.1,
67
+ "workflow_fit": 4.8,
68
+ },
69
+ "Sidecar-Evolution": {
70
+ "leverage": 3.5,
71
+ "reusability": 4.4,
72
+ "strategic_value": 4.8,
73
+ "product_readiness": 2.3,
74
+ "synergy": 4.6,
75
+ "workflow_fit": 3.8,
76
+ },
77
+ "AgentArena": {
78
+ "leverage": 3.9,
79
+ "reusability": 4.1,
80
+ "strategic_value": 4.3,
81
+ "product_readiness": 3.8,
82
+ "synergy": 4.0,
83
+ "workflow_fit": 3.7,
84
+ },
85
+ }
86
+ PROJECT_NOTES = {
87
+ "Sidecar-ng": "Stateless Hook statt Daemon, direkte Claude-Code-Integration, dokumentierte Evals und klares Accuracy-Signal.",
88
+ "MyAIGame": "Breites Workflow-Toolkit mit Skills, Hooks, Session-Browser und MCP-Setups als Multiplikator fuer andere Projekte.",
89
+ "Sidecar-V7": "Reifste volle Sidecar-Ausbaustufe mit hoher Produktnaehe, aber auch sichtbarer Komplexitaetslast.",
90
+ "ClaudeCodePanel": "Starke Observability-Schicht fuer Sessions, Kosten, Swarms und Events mit direktem Nutzwert im Alltag.",
91
+ "Sidecar-Evolution": "Meta-Repo fuer KEEP/PARK/KILL-Entscheidungen und Evaluationsarchaeologie, strategisch stark aber indirekt.",
92
+ "AgentArena": "Eval-Harness mit Benchmark-Logik und Visual-Explainer-Potenzial, aber weniger direkt in den taeglichen Workflow eingebettet.",
93
+ }
94
+
95
+
96
+ @dataclass
97
+ class Session:
98
+ start: datetime
99
+ end: datetime
100
+ commit_count: int
101
+
102
+ @property
103
+ def span_hours(self) -> float:
104
+ return max(0.0, (self.end - self.start).total_seconds() / 3600)
105
+
106
+ @property
107
+ def estimate_hours(self) -> float:
108
+ padded = self.span_hours + 0.65 + max(0, self.commit_count - 1) * 0.18
109
+ return max(0.5, min(4.5, padded))
110
+
111
+
112
+ def run_git(repo: Path, *args: str) -> str:
113
+ result = subprocess.run(
114
+ ["git", "-C", str(repo), *args],
115
+ check=False,
116
+ capture_output=True,
117
+ text=True,
118
+ )
119
+ return result.stdout.strip()
120
+
121
+
122
+ def list_repos() -> list[Path]:
123
+ repos = []
124
+ for entry in sorted(ROOT.iterdir()):
125
+ if not entry.is_dir() or entry.name.startswith("."):
126
+ continue
127
+ if (entry / ".git").is_dir():
128
+ repos.append(entry)
129
+ return repos
130
+
131
+
132
+ def parse_log(repo: Path) -> list[dict[str, Any]]:
133
+ out = run_git(
134
+ repo,
135
+ "log",
136
+ f"--since={WINDOW_START.isoformat(sep=' ')}",
137
+ f"--until={WINDOW_END.isoformat(sep=' ')}",
138
+ "--date=iso-strict",
139
+ "--pretty=format:%ad%x1f%s%x1f%h",
140
+ )
141
+ commits = []
142
+ for line in out.splitlines():
143
+ if not line.strip():
144
+ continue
145
+ date_str, subject, short_hash = line.split("\x1f")
146
+ ts = datetime.fromisoformat(date_str)
147
+ commits.append(
148
+ {
149
+ "timestamp": ts,
150
+ "date": ts.strftime("%Y-%m-%d"),
151
+ "time": ts.strftime("%H:%M"),
152
+ "subject": subject,
153
+ "hash": short_hash,
154
+ }
155
+ )
156
+ return commits
157
+
158
+
159
+ def build_sessions(commits: list[dict[str, Any]]) -> list[Session]:
160
+ if not commits:
161
+ return []
162
+ ordered = sorted((c["timestamp"] for c in commits))
163
+ sessions: list[Session] = []
164
+ current = [ordered[0]]
165
+ for ts in ordered[1:]:
166
+ if (ts - current[-1]).total_seconds() / 60 > SESSION_GAP_MINUTES:
167
+ sessions.append(Session(current[0], current[-1], len(current)))
168
+ current = [ts]
169
+ else:
170
+ current.append(ts)
171
+ sessions.append(Session(current[0], current[-1], len(current)))
172
+ return sessions
173
+
174
+
175
+ def find_artifacts(repo: Path) -> list[Path]:
176
+ artifacts = []
177
+ patterns = ("README", "PLAN", "REVIEW", "BACKLOG", "FOR_SMLFLG")
178
+ for path in repo.rglob("*"):
179
+ if path.is_dir():
180
+ continue
181
+ if ".git" in path.parts:
182
+ continue
183
+ if len(path.relative_to(repo).parts) > 3:
184
+ continue
185
+ if any(path.name.upper().startswith(prefix) for prefix in patterns):
186
+ artifacts.append(path)
187
+ return sorted(artifacts)
188
+
189
+
190
+ def latest_activity(repo: Path) -> str | None:
191
+ latest = 0.0
192
+ for path in repo.rglob("*"):
193
+ if ".git" in path.parts:
194
+ continue
195
+ try:
196
+ if path.is_file():
197
+ latest = max(latest, path.stat().st_mtime)
198
+ except OSError:
199
+ continue
200
+ if not latest:
201
+ return None
202
+ return datetime.fromtimestamp(latest).strftime("%Y-%m-%d %H:%M")
203
+
204
+
205
+ def in_window(date_text: str | None) -> bool:
206
+ if not date_text:
207
+ return False
208
+ ts = datetime.strptime(date_text, "%Y-%m-%d %H:%M")
209
+ return WINDOW_START.replace(tzinfo=None) <= ts <= WINDOW_END.replace(tzinfo=None)
210
+
211
+
212
+ def classify_project(repo: Path, artifacts: list[Path]) -> tuple[str, str]:
213
+ names = {p.name.lower() for p in repo.iterdir()}
214
+ text = " ".join(path.name.lower() for path in artifacts)
215
+ repo_name = repo.name.lower()
216
+ if "sidecar" in repo_name:
217
+ return "Sidecar / Meta Tooling", "Agent workflow instrumentation, hooks, evals or platforming."
218
+ if any(token in repo_name for token in ("agent", "swarm", "arena")):
219
+ return "Agent Infrastructure", "Agent evaluation, orchestration or execution surface."
220
+ if {"templates", "static"}.issubset(names) or "htmx" in text:
221
+ return "Vertical App", "Local web app pattern with templates/static/tests suggests end-user MVPs."
222
+ if any(token in repo_name for token in ("panel", "dashboard", "monitor", "counter", "voicemode")):
223
+ return "Personal Workflow OS", "Observability or desktop support tooling for your own coding environment."
224
+ if any(token in repo_name for token in ("prompt", "writing", "bookmark")):
225
+ return "Knowledge / Content", "Prompt, content or knowledge capture system."
226
+ return "Tooling", "General-purpose local project tooling."
227
+
228
+
229
+ def summarize_commit_types(commits: list[dict[str, Any]]) -> dict[str, int]:
230
+ counts = Counter()
231
+ for commit in commits:
232
+ subject = commit["subject"].lower()
233
+ for key in ("feat", "fix", "test", "docs", "refactor", "chore"):
234
+ if subject.startswith(f"{key}:") or subject.startswith(f"{key}(") or subject.startswith(f"{key} "):
235
+ counts[key] += 1
236
+ break
237
+ else:
238
+ counts["other"] += 1
239
+ return dict(counts)
240
+
241
+
242
+ def has_uncommitted_work(repo: Path) -> bool:
243
+ return bool(run_git(repo, "status", "--short"))
244
+
245
+
246
+ def repo_summary(repo: Path) -> dict[str, Any]:
247
+ commits = parse_log(repo)
248
+ sessions = build_sessions(commits)
249
+ artifacts = find_artifacts(repo)
250
+ cluster, cluster_reason = classify_project(repo, artifacts)
251
+ branch = run_git(repo, "rev-parse", "--abbrev-ref", "HEAD") or "unknown"
252
+ status = run_git(repo, "status", "--short")
253
+ status_label = "clean"
254
+ if status:
255
+ status_label = "dirty" if any(not line.startswith("??") for line in status.splitlines()) else "untracked-only"
256
+ commit_days = sorted({c["date"] for c in commits})
257
+ commit_type_counts = summarize_commit_types(commits)
258
+ plan_count = sum(1 for path in artifacts if path.name.upper().startswith("PLAN"))
259
+ review_count = sum(1 for path in artifacts if path.name.upper().startswith("REVIEW"))
260
+ readme_count = sum(1 for path in artifacts if path.name.upper().startswith("README"))
261
+ backlog_count = sum(1 for path in artifacts if path.name.upper().startswith("BACKLOG"))
262
+ has_tests = (repo / "tests").exists() or (repo / "e2e").exists()
263
+ has_eval = (repo / "eval").exists()
264
+ has_docs = (repo / "docs").exists()
265
+ return {
266
+ "name": repo.name,
267
+ "path": str(repo),
268
+ "branch": branch,
269
+ "status": status_label,
270
+ "has_uncommitted_work": has_uncommitted_work(repo),
271
+ "latest_activity": latest_activity(repo),
272
+ "cluster": cluster,
273
+ "cluster_reason": cluster_reason,
274
+ "commit_count": len(commits),
275
+ "commit_days": commit_days,
276
+ "commit_day_count": len(commit_days),
277
+ "commits_sample": [
278
+ {"date": c["date"], "time": c["time"], "subject": c["subject"], "hash": c["hash"]}
279
+ for c in commits[:8]
280
+ ],
281
+ "first_commit": commits[-1]["date"] if commits else None,
282
+ "last_commit": commits[0]["date"] if commits else None,
283
+ "artifact_counts": {
284
+ "readme": readme_count,
285
+ "plan": plan_count,
286
+ "review": review_count,
287
+ "backlog": backlog_count,
288
+ },
289
+ "structure_markers": {
290
+ "tests": has_tests,
291
+ "eval": has_eval,
292
+ "docs": has_docs,
293
+ "systemd": (repo / "systemd").exists(),
294
+ "knowledge": (repo / "knowledge").exists(),
295
+ "adapters": (repo / "adapters").exists(),
296
+ "core": (repo / "core").exists(),
297
+ "samuel": (repo / "samuel").exists(),
298
+ },
299
+ "commit_type_counts": commit_type_counts,
300
+ "session_count": len(sessions),
301
+ "sessions": [
302
+ {
303
+ "start": s.start.isoformat(),
304
+ "end": s.end.isoformat(),
305
+ "commit_count": s.commit_count,
306
+ "estimate_hours": round(s.estimate_hours, 2),
307
+ }
308
+ for s in sessions
309
+ ],
310
+ "estimated_hours": round(sum(s.estimate_hours for s in sessions), 2),
311
+ }
312
+
313
+
314
+ def estimate_work_hours(projects: list[dict[str, Any]]) -> dict[str, Any]:
315
+ active = [
316
+ p
317
+ for p in projects
318
+ if p["commit_count"] > 0 or (p["has_uncommitted_work"] and in_window(p["latest_activity"]))
319
+ ]
320
+ session_hours = sum(p["estimated_hours"] for p in active)
321
+ artifact_bonus = 0.0
322
+ uncertainty = []
323
+ for project in active:
324
+ artifact_count = sum(project["artifact_counts"].values())
325
+ if artifact_count:
326
+ artifact_bonus += min(1.0, 0.15 * artifact_count)
327
+ if project["has_uncommitted_work"] and project["commit_count"] == 0 and in_window(project["latest_activity"]):
328
+ artifact_bonus += 0.75
329
+ uncertainty.append(f"{project['name']} hatte lokale Aktivitaet ohne Commit-Spur im Fenster.")
330
+ best = round(session_hours + artifact_bonus, 1)
331
+ lower = round(max(1.0, session_hours * 0.72), 1)
332
+ upper = round(best * 1.38 + 4.0, 1)
333
+ return {
334
+ "best_estimate_hours": best,
335
+ "lower_bound_hours": lower,
336
+ "upper_bound_hours": upper,
337
+ "active_project_count": len(active),
338
+ "method": [
339
+ "Commit-Timestamps im Fenster 2026-02-19 bis 2026-03-20 zu Sessions gebuendelt (neue Session ab >120 Minuten Luecke).",
340
+ "Pro Session Schaetzung aus beobachtetem Commit-Span plus Puffer fuer Vor-/Nacharbeit, gedeckelt um einzelne Squash- oder Burst-Sessions nicht zu uebergewichten.",
341
+ "Zusatzbonus fuer PLAN/REVIEW/BACKLOG-Artefakte und uncommitted Aktivitaet, weil Cloud-Code-Arbeit oft ausserhalb des finalen Commits sichtbar wird.",
342
+ ],
343
+ "uncertainties": uncertainty
344
+ + [
345
+ "Nicht jede Arbeitsminute landet in Git; besonders lokale Experimente, Copy/Paste, Prompting und verworfene Ansaetze fehlen teilweise.",
346
+ "Einige Commits koennen squash-/batch-artig sein und daher mehr reale Arbeit reprasentieren als die Zeitdifferenzen zeigen.",
347
+ ],
348
+ }
349
+
350
+
351
+ def build_timeline(projects: list[dict[str, Any]]) -> list[dict[str, Any]]:
352
+ days = []
353
+ cursor = WINDOW_START.date()
354
+ end_date = WINDOW_END.date()
355
+ while cursor <= end_date:
356
+ day_projects = []
357
+ commit_total = 0
358
+ for project in projects:
359
+ matches = [c for c in project["commits_sample"]] # placeholder for schema consistency
360
+ count = sum(1 for session in project["sessions"] if session["start"][:10] == cursor.isoformat())
361
+ if count:
362
+ day_projects.append({"name": project["name"], "session_count": count})
363
+ commit_total += sum(1 for d in project["commit_days"] if d == cursor.isoformat())
364
+ intensity = min(4, commit_total)
365
+ days.append(
366
+ {
367
+ "date": cursor.isoformat(),
368
+ "commit_days": commit_total,
369
+ "intensity": intensity,
370
+ "projects": day_projects,
371
+ }
372
+ )
373
+ cursor += timedelta(days=1)
374
+ return days
375
+
376
+
377
+ def workflow_patterns(projects: list[dict[str, Any]]) -> dict[str, Any]:
378
+ commit_types = Counter()
379
+ active_projects = [p for p in projects if p["commit_count"] > 0]
380
+ day_to_projects: dict[str, set[str]] = defaultdict(set)
381
+ for project in active_projects:
382
+ commit_types.update(project["commit_type_counts"])
383
+ for day in project["commit_days"]:
384
+ day_to_projects[day].add(project["name"])
385
+ multi_project_days = sum(1 for names in day_to_projects.values() if len(names) >= 2)
386
+ prototype_signals = commit_types["feat"] + commit_types["other"]
387
+ refinement_signals = commit_types["fix"] + commit_types["test"] + commit_types["refactor"] + commit_types["docs"]
388
+ eval_projects = [
389
+ p["name"]
390
+ for p in projects
391
+ if p["structure_markers"]["eval"] or p["artifact_counts"]["review"] or p["commit_type_counts"].get("test", 0)
392
+ ]
393
+ top_active = sorted(active_projects, key=lambda p: p["commit_count"], reverse=True)[:5]
394
+ return {
395
+ "startup_pattern": "Du startest Projekte meist mit schriftlicher Rahmung: READMEs, PLAN-Dateien und Kontext-Artefakte tauchen sehr frueh auf und bleiben als Steuerungsoberflaeche erhalten.",
396
+ "iteration_pattern": "Die dominante Schleife ist nicht linearer Build, sondern schnelle Zyklen aus feat/fix/test/docs. Sidecar-Projekte zeigen besonders stark: erst Scope aufspannen, dann messen, dann radikal zusammenstreichen.",
397
+ "parallel_vs_serial": {
398
+ "label": "Hybrid mit Sidecar als seriellem Hauptstrang",
399
+ "evidence": f"{multi_project_days} Tage im Fenster zeigen Aktivitaet in mindestens zwei Projekten; gleichzeitig konzentrieren sich die meisten Commits auf wenige Kernrepos mit Sidecar-ng, Sidecar-V7, ClaudeCodePanel und AgentArena.",
400
+ },
401
+ "prototyping_vs_refinement": {
402
+ "prototype_signals": prototype_signals,
403
+ "refinement_signals": refinement_signals,
404
+ "interpretation": "Der Monat kippt nach fruehem Scope-Aufbau deutlich in Refinement: Tests, Doku, Refactors und Eval-Artefakte sind in den aktiven Kernprojekten stark sichtbar.",
405
+ },
406
+ "evaluation_loops": {
407
+ "projects": eval_projects[:8],
408
+ "interpretation": "Eval-/Review-Loops sind kein Nachgedanke. AgentArena, Sidecar-ng, Sidecar-Evolution und ClaudeCodePanel zeigen Mess-, Review- oder Validierungsartefakte direkt im Repo.",
409
+ },
410
+ "preferred_project_types": [
411
+ "Agentische Infrastruktur und Eval-Harnesses",
412
+ "Persoenliche Workflow- und Observability-Tools",
413
+ "Lokale Python-Web-MVPs mit templates/static/tests",
414
+ ],
415
+ "meta_systems": [
416
+ "Selbstinstrumentierung des eigenen Entwicklungsprozesses",
417
+ "Core-vs-Personalization-Trennung in reiferen Systemen",
418
+ "Planungsrepo und Implementierungsrepo bewusst auseinanderziehen",
419
+ ],
420
+ "featured_projects": [
421
+ {"name": project["name"], "commit_count": project["commit_count"], "cluster": project["cluster"]}
422
+ for project in top_active
423
+ ],
424
+ }
425
+
426
+
427
+ def ranking(projects: list[dict[str, Any]]) -> dict[str, Any]:
428
+ lookup = {project["name"]: project for project in projects}
429
+ rows = []
430
+ for name in TOP_RECOMMENDATION_ORDER:
431
+ if name not in lookup:
432
+ continue
433
+ scores = RANKING_SCORES[name]
434
+ weighted = round(
435
+ (
436
+ scores["leverage"] * 0.24
437
+ + scores["reusability"] * 0.16
438
+ + scores["strategic_value"] * 0.18
439
+ + scores["product_readiness"] * 0.14
440
+ + scores["synergy"] * 0.16
441
+ + scores["workflow_fit"] * 0.12
442
+ ),
443
+ 2,
444
+ )
445
+ project = lookup[name]
446
+ rows.append(
447
+ {
448
+ "name": name,
449
+ "score": weighted,
450
+ "scores": scores,
451
+ "commit_count": project["commit_count"],
452
+ "last_commit": project["last_commit"],
453
+ "summary": PROJECT_NOTES[name],
454
+ }
455
+ )
456
+ rows.sort(key=lambda row: row["score"], reverse=True)
457
+ return {
458
+ "top_recommendation": rows[0],
459
+ "top_three": rows[:3],
460
+ "shortlist": rows[:5],
461
+ "method": "Editorial scoring auf sechs Kriterien, kombiniert mit Aktivitaetssignalen, Doku-Dichte und Synergie zum Cloud-Code-Workflow.",
462
+ }
463
+
464
+
465
+ def build_executive_summary(projects: list[dict[str, Any]], work_estimate: dict[str, Any], rank: dict[str, Any]) -> dict[str, Any]:
466
+ active = sorted(
467
+ (
468
+ p
469
+ for p in projects
470
+ if p["commit_count"] > 0 or (p["has_uncommitted_work"] and in_window(p["latest_activity"]))
471
+ ),
472
+ key=lambda p: p["commit_count"],
473
+ reverse=True,
474
+ )
475
+ headline = (
476
+ f"Im 30-Tage-Fenster liegen geschaetzt {work_estimate['best_estimate_hours']} Stunden Cloud-Code-Arbeit "
477
+ f"ueber {work_estimate['active_project_count']} aktiv sichtbare Projekte."
478
+ )
479
+ return {
480
+ "headline": headline,
481
+ "key_findings": [
482
+ "Der Monat war kein einzelnes Produktprojekt, sondern ein Portfolio aus Meta-Tooling, Evaluationssystemen und persoenlichen Workflow-Oberflaechen.",
483
+ "Die staerkste Prozesssignatur ist instrumentierte Iteration: PLAN/REVIEW/EVAL-Dateien und test-/docs-lastige Commits begleiten die Umsetzung eng.",
484
+ f"Die beste Weiterentwicklungswette ist {rank['top_recommendation']['name']}, weil dort Hebel, Reifegrad und direkter Workflow-Fit am staerksten zusammenfallen.",
485
+ ],
486
+ "top_activity": [
487
+ {
488
+ "name": project["name"],
489
+ "commit_count": project["commit_count"],
490
+ "session_count": project["session_count"],
491
+ "cluster": project["cluster"],
492
+ }
493
+ for project in active[:6]
494
+ ],
495
+ }
496
+
497
+
498
+ def build_clusters(projects: list[dict[str, Any]]) -> list[dict[str, Any]]:
499
+ grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
500
+ for project in projects:
501
+ grouped[project["cluster"]].append(project)
502
+ output = []
503
+ for cluster, items in grouped.items():
504
+ output.append(
505
+ {
506
+ "name": cluster,
507
+ "count": len(items),
508
+ "projects": [
509
+ {
510
+ "name": item["name"],
511
+ "commit_count": item["commit_count"],
512
+ "status": item["status"],
513
+ }
514
+ for item in sorted(items, key=lambda p: p["commit_count"], reverse=True)[:8]
515
+ ],
516
+ "summary": items[0]["cluster_reason"],
517
+ }
518
+ )
519
+ output.sort(key=lambda item: item["count"], reverse=True)
520
+ return output
521
+
522
+
523
+ def build_evidence(projects: list[dict[str, Any]]) -> list[dict[str, Any]]:
524
+ lookup = {p["name"]: p for p in projects}
525
+ evidence_rows = []
526
+ for name in ["Sidecar-ng", "Sidecar-V7", "ClaudeCodePanel", "AgentArena", "MyAIGame", "Sidecar-Evolution", "Sidecar-V8"]:
527
+ project = lookup.get(name)
528
+ if not project:
529
+ continue
530
+ evidence_rows.append(
531
+ {
532
+ "project": name,
533
+ "commits": project["commit_count"],
534
+ "last_commit": project["last_commit"],
535
+ "artifacts": project["artifact_counts"],
536
+ "markers": project["structure_markers"],
537
+ "sample_messages": [entry["subject"] for entry in project["commits_sample"][:3]],
538
+ }
539
+ )
540
+ return evidence_rows
541
+
542
+
543
+ def build_payload() -> dict[str, Any]:
544
+ repos = [repo for repo in list_repos() if repo.name not in IGNORED_PROJECTS]
545
+ projects = [repo_summary(repo) for repo in repos]
546
+ projects.sort(key=lambda project: (project["commit_count"], project["estimated_hours"]), reverse=True)
547
+ work_estimate = estimate_work_hours(projects)
548
+ rank = ranking(projects)
549
+ return {
550
+ "meta": {
551
+ "generated_at": datetime.now().isoformat(timespec="seconds"),
552
+ "source_root": str(ROOT),
553
+ "window_start": WINDOW_START.date().isoformat(),
554
+ "window_end": WINDOW_END.date().isoformat(),
555
+ "ignored_projects": sorted(IGNORED_PROJECTS),
556
+ "project_count": len(projects),
557
+ },
558
+ "executive_summary": build_executive_summary(projects, work_estimate, rank),
559
+ "work_estimate": work_estimate,
560
+ "workflow_patterns": workflow_patterns(projects),
561
+ "project_clusters": build_clusters(projects),
562
+ "timeline": build_timeline(projects),
563
+ "rankings": rank,
564
+ "evidence": build_evidence(projects),
565
+ "projects": projects,
566
+ }
567
+
568
+
569
+ def main() -> None:
570
+ payload = build_payload()
571
+ OUT.parent.mkdir(parents=True, exist_ok=True)
572
+ OUT.write_text(json.dumps(payload, indent=2), encoding="utf-8")
573
+ print(f"Wrote {OUT}")
574
+
575
+
576
+ if __name__ == "__main__":
577
+ main()
styles.css ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #07111d;
3
+ --bg-soft: #0d1827;
4
+ --panel: rgba(12, 23, 38, 0.84);
5
+ --panel-strong: rgba(17, 31, 49, 0.94);
6
+ --line: rgba(151, 184, 214, 0.17);
7
+ --text: #edf3fb;
8
+ --muted: #97aac0;
9
+ --gold: #f0cf8e;
10
+ --teal: #77dfd0;
11
+ --green: #91e5a5;
12
+ --amber: #f4c16e;
13
+ --red: #f08f88;
14
+ --shadow: 0 24px 60px rgba(0, 0, 0, 0.34);
15
+ --radius: 24px;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ html {
23
+ scroll-behavior: smooth;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ color: var(--text);
29
+ background:
30
+ radial-gradient(circle at top left, rgba(47, 120, 173, 0.2), transparent 32%),
31
+ radial-gradient(circle at top right, rgba(240, 207, 142, 0.12), transparent 28%),
32
+ linear-gradient(180deg, #08111d 0%, #07111d 44%, #0a1522 100%);
33
+ font-family: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif;
34
+ }
35
+
36
+ body::before {
37
+ content: "";
38
+ position: fixed;
39
+ inset: 0;
40
+ pointer-events: none;
41
+ background-image:
42
+ linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
43
+ linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
44
+ background-size: 48px 48px;
45
+ mask-image: linear-gradient(180deg, black 35%, transparent 100%);
46
+ }
47
+
48
+ .page-shell {
49
+ width: min(1200px, calc(100vw - 32px));
50
+ margin: 0 auto;
51
+ padding: 36px 0 80px;
52
+ }
53
+
54
+ .hero {
55
+ display: grid;
56
+ grid-template-columns: 1.5fr 0.9fr;
57
+ gap: 24px;
58
+ align-items: stretch;
59
+ margin-bottom: 40px;
60
+ }
61
+
62
+ .hero-copy,
63
+ .hero-panel,
64
+ .panel,
65
+ .card,
66
+ .cluster-card,
67
+ .evidence-card {
68
+ border: 1px solid var(--line);
69
+ background: var(--panel);
70
+ box-shadow: var(--shadow);
71
+ backdrop-filter: blur(18px);
72
+ }
73
+
74
+ .hero-copy {
75
+ border-radius: 28px;
76
+ padding: 36px;
77
+ min-height: 320px;
78
+ }
79
+
80
+ .hero-panel,
81
+ .panel {
82
+ border-radius: var(--radius);
83
+ padding: 24px;
84
+ }
85
+
86
+ .eyebrow,
87
+ .kicker {
88
+ margin: 0 0 10px;
89
+ color: var(--teal);
90
+ font-size: 0.82rem;
91
+ text-transform: uppercase;
92
+ letter-spacing: 0.18em;
93
+ }
94
+
95
+ h1,
96
+ h2,
97
+ h3 {
98
+ font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
99
+ font-weight: 600;
100
+ letter-spacing: -0.03em;
101
+ }
102
+
103
+ h1 {
104
+ font-size: clamp(2.6rem, 6vw, 5rem);
105
+ line-height: 0.95;
106
+ margin: 0 0 18px;
107
+ max-width: 12ch;
108
+ }
109
+
110
+ h2 {
111
+ margin: 0;
112
+ font-size: clamp(1.8rem, 3vw, 2.7rem);
113
+ }
114
+
115
+ h3 {
116
+ margin-top: 0;
117
+ font-size: 1.25rem;
118
+ }
119
+
120
+ .lede {
121
+ color: var(--muted);
122
+ font-size: 1.08rem;
123
+ line-height: 1.7;
124
+ max-width: 58ch;
125
+ }
126
+
127
+ .summary-stat {
128
+ display: flex;
129
+ align-items: baseline;
130
+ justify-content: space-between;
131
+ padding: 16px 0;
132
+ border-bottom: 1px solid var(--line);
133
+ }
134
+
135
+ .summary-stat:last-child {
136
+ border-bottom: 0;
137
+ padding-bottom: 0;
138
+ }
139
+
140
+ .summary-stat strong {
141
+ font-size: 1.8rem;
142
+ font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
143
+ }
144
+
145
+ .summary-stat span {
146
+ color: var(--muted);
147
+ }
148
+
149
+ .section {
150
+ margin-top: 34px;
151
+ }
152
+
153
+ .section-heading {
154
+ display: flex;
155
+ justify-content: space-between;
156
+ gap: 20px;
157
+ align-items: end;
158
+ margin-bottom: 18px;
159
+ }
160
+
161
+ .cards {
162
+ display: grid;
163
+ gap: 18px;
164
+ }
165
+
166
+ .cards.three {
167
+ grid-template-columns: repeat(3, minmax(0, 1fr));
168
+ }
169
+
170
+ .cards.four {
171
+ grid-template-columns: repeat(4, minmax(0, 1fr));
172
+ }
173
+
174
+ .card,
175
+ .cluster-card,
176
+ .evidence-card {
177
+ border-radius: 22px;
178
+ padding: 22px;
179
+ }
180
+
181
+ .card .label,
182
+ .cluster-card .label {
183
+ color: var(--muted);
184
+ text-transform: uppercase;
185
+ letter-spacing: 0.14em;
186
+ font-size: 0.74rem;
187
+ margin-bottom: 12px;
188
+ }
189
+
190
+ .metric {
191
+ font-size: 2.4rem;
192
+ font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
193
+ margin: 0 0 8px;
194
+ }
195
+
196
+ .supporting {
197
+ color: var(--muted);
198
+ line-height: 1.65;
199
+ }
200
+
201
+ .split {
202
+ display: grid;
203
+ grid-template-columns: repeat(2, minmax(0, 1fr));
204
+ gap: 18px;
205
+ margin-top: 18px;
206
+ }
207
+
208
+ .process-layout {
209
+ align-items: stretch;
210
+ }
211
+
212
+ .narrative p,
213
+ .bullet-list li,
214
+ .subnote,
215
+ table {
216
+ color: var(--muted);
217
+ line-height: 1.65;
218
+ }
219
+
220
+ .bullet-list {
221
+ margin: 0;
222
+ padding-left: 18px;
223
+ }
224
+
225
+ .ratio-chart {
226
+ display: grid;
227
+ gap: 12px;
228
+ }
229
+
230
+ .ratio-bar {
231
+ position: relative;
232
+ height: 18px;
233
+ border-radius: 999px;
234
+ overflow: hidden;
235
+ background: rgba(255, 255, 255, 0.06);
236
+ }
237
+
238
+ .ratio-bar span {
239
+ display: block;
240
+ height: 100%;
241
+ }
242
+
243
+ .proto {
244
+ background: linear-gradient(90deg, #f0cf8e, #e59d54);
245
+ }
246
+
247
+ .refine {
248
+ background: linear-gradient(90deg, #77dfd0, #56a9df);
249
+ }
250
+
251
+ .mini-stack {
252
+ display: grid;
253
+ gap: 10px;
254
+ margin-top: 18px;
255
+ }
256
+
257
+ .mini-row {
258
+ display: grid;
259
+ grid-template-columns: 110px 1fr auto;
260
+ gap: 12px;
261
+ align-items: center;
262
+ }
263
+
264
+ .track {
265
+ height: 10px;
266
+ border-radius: 999px;
267
+ overflow: hidden;
268
+ background: rgba(255, 255, 255, 0.06);
269
+ }
270
+
271
+ .track > span {
272
+ display: block;
273
+ height: 100%;
274
+ border-radius: inherit;
275
+ background: linear-gradient(90deg, var(--teal), var(--green));
276
+ }
277
+
278
+ .cluster-grid,
279
+ .evidence-grid {
280
+ display: grid;
281
+ grid-template-columns: repeat(3, minmax(0, 1fr));
282
+ gap: 18px;
283
+ }
284
+
285
+ .pill-row {
286
+ display: flex;
287
+ flex-wrap: wrap;
288
+ gap: 8px;
289
+ margin-top: 12px;
290
+ }
291
+
292
+ .pill {
293
+ padding: 7px 10px;
294
+ border-radius: 999px;
295
+ background: rgba(255, 255, 255, 0.06);
296
+ color: var(--text);
297
+ font-size: 0.86rem;
298
+ }
299
+
300
+ table {
301
+ width: 100%;
302
+ border-collapse: collapse;
303
+ }
304
+
305
+ th,
306
+ td {
307
+ text-align: left;
308
+ padding: 12px 10px;
309
+ border-bottom: 1px solid var(--line);
310
+ vertical-align: top;
311
+ }
312
+
313
+ th {
314
+ color: var(--text);
315
+ font-size: 0.84rem;
316
+ text-transform: uppercase;
317
+ letter-spacing: 0.12em;
318
+ }
319
+
320
+ .timeline-grid {
321
+ display: grid;
322
+ grid-template-columns: repeat(10, minmax(0, 1fr));
323
+ gap: 10px;
324
+ }
325
+
326
+ .day-cell {
327
+ padding: 12px;
328
+ border-radius: 18px;
329
+ min-height: 92px;
330
+ border: 1px solid var(--line);
331
+ background: rgba(255, 255, 255, 0.03);
332
+ }
333
+
334
+ .day-cell.level-0 {
335
+ opacity: 0.42;
336
+ }
337
+
338
+ .day-cell.level-1 {
339
+ background: linear-gradient(180deg, rgba(119, 223, 208, 0.1), rgba(255, 255, 255, 0.02));
340
+ }
341
+
342
+ .day-cell.level-2 {
343
+ background: linear-gradient(180deg, rgba(119, 223, 208, 0.18), rgba(255, 255, 255, 0.03));
344
+ }
345
+
346
+ .day-cell.level-3,
347
+ .day-cell.level-4 {
348
+ background: linear-gradient(180deg, rgba(240, 207, 142, 0.2), rgba(255, 255, 255, 0.04));
349
+ }
350
+
351
+ .day-head {
352
+ display: flex;
353
+ justify-content: space-between;
354
+ gap: 10px;
355
+ margin-bottom: 8px;
356
+ color: var(--muted);
357
+ font-size: 0.82rem;
358
+ }
359
+
360
+ .day-projects {
361
+ display: grid;
362
+ gap: 6px;
363
+ font-size: 0.84rem;
364
+ }
365
+
366
+ .recommendation-main .badge {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ gap: 10px;
370
+ padding: 8px 12px;
371
+ border-radius: 999px;
372
+ background: rgba(119, 223, 208, 0.1);
373
+ color: var(--teal);
374
+ font-size: 0.84rem;
375
+ text-transform: uppercase;
376
+ letter-spacing: 0.12em;
377
+ margin-bottom: 14px;
378
+ }
379
+
380
+ .rank-item {
381
+ padding: 14px 0;
382
+ border-bottom: 1px solid var(--line);
383
+ }
384
+
385
+ .rank-item:last-child {
386
+ border-bottom: 0;
387
+ }
388
+
389
+ .score {
390
+ font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
391
+ font-size: 1.35rem;
392
+ }
393
+
394
+ .muted {
395
+ color: var(--muted);
396
+ }
397
+
398
+ @media (max-width: 980px) {
399
+ .hero,
400
+ .split,
401
+ .cards.three,
402
+ .cards.four,
403
+ .cluster-grid,
404
+ .evidence-grid {
405
+ grid-template-columns: 1fr;
406
+ }
407
+
408
+ .timeline-grid {
409
+ grid-template-columns: repeat(2, minmax(0, 1fr));
410
+ }
411
+ }
412
+
413
+ @media (max-width: 640px) {
414
+ .page-shell {
415
+ width: min(100vw - 20px, 1200px);
416
+ padding-top: 18px;
417
+ }
418
+
419
+ .hero-copy,
420
+ .hero-panel,
421
+ .panel,
422
+ .card,
423
+ .cluster-card,
424
+ .evidence-card {
425
+ padding: 18px;
426
+ }
427
+
428
+ .timeline-grid {
429
+ grid-template-columns: 1fr;
430
+ }
431
+ }