gsd-pi 2.26.0 → 2.27.0
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/README.md +43 -6
- package/dist/cli.js +4 -2
- package/dist/headless.d.ts +3 -0
- package/dist/headless.js +136 -8
- package/dist/help-text.js +3 -0
- package/dist/loader.js +33 -4
- package/dist/resources/extensions/bg-shell/index.ts +19 -2
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/gsd/auto/session.ts +224 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/dist/resources/extensions/gsd/auto.ts +977 -1551
- package/dist/resources/extensions/gsd/commands.ts +3 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/export-html.ts +1001 -0
- package/dist/resources/extensions/gsd/export.ts +49 -1
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
- package/dist/resources/extensions/gsd/index.ts +54 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/preferences.ts +62 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/reports.ts +510 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/types.ts +38 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/dist/resources/extensions/shared/format-utils.ts +85 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/subagent/index.ts +46 -1
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/scripts/link-workspace-packages.cjs +22 -6
- package/src/resources/extensions/bg-shell/index.ts +19 -2
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/gsd/auto/session.ts +224 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/src/resources/extensions/gsd/auto.ts +977 -1551
- package/src/resources/extensions/gsd/commands.ts +3 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/export-html.ts +1001 -0
- package/src/resources/extensions/gsd/export.ts +49 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -5
- package/src/resources/extensions/gsd/index.ts +54 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/src/resources/extensions/gsd/observability-validator.ts +21 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/preferences.ts +62 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/reports.ts +510 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/types.ts +38 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/src/resources/extensions/gsd/verification-gate.ts +567 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/src/resources/extensions/shared/format-utils.ts +85 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/subagent/index.ts +46 -1
- package/src/resources/extensions/subagent/isolation.ts +9 -6
|
@@ -75,7 +75,11 @@ const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
75
75
|
"token_profile",
|
|
76
76
|
"phases",
|
|
77
77
|
"auto_visualize",
|
|
78
|
+
"auto_report",
|
|
78
79
|
"parallel",
|
|
80
|
+
"verification_commands",
|
|
81
|
+
"verification_auto_fix",
|
|
82
|
+
"verification_max_retries",
|
|
79
83
|
]);
|
|
80
84
|
|
|
81
85
|
export interface GSDSkillRule {
|
|
@@ -172,7 +176,12 @@ export interface GSDPreferences {
|
|
|
172
176
|
token_profile?: TokenProfile;
|
|
173
177
|
phases?: PhaseSkipPreferences;
|
|
174
178
|
auto_visualize?: boolean;
|
|
179
|
+
/** Generate HTML report snapshot after each milestone completion. Default: true. Set false to disable. */
|
|
180
|
+
auto_report?: boolean;
|
|
175
181
|
parallel?: import("./types.js").ParallelConfig;
|
|
182
|
+
verification_commands?: string[];
|
|
183
|
+
verification_auto_fix?: boolean;
|
|
184
|
+
verification_max_retries?: number;
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
export interface LoadedGSDPreferences {
|
|
@@ -327,7 +336,7 @@ function resolveSkillReference(ref: string, cwd: string): SkillResolution {
|
|
|
327
336
|
try {
|
|
328
337
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
329
338
|
for (const entry of entries) {
|
|
330
|
-
if (!entry.isDirectory()) continue;
|
|
339
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
331
340
|
if (entry.name === expanded) {
|
|
332
341
|
const skillFile = join(dir, entry.name, "SKILL.md");
|
|
333
342
|
if (existsSync(skillFile)) {
|
|
@@ -586,6 +595,18 @@ export function getNextFallbackModel(
|
|
|
586
595
|
}
|
|
587
596
|
}
|
|
588
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Detect whether an error message indicates a transient network error
|
|
600
|
+
* (worth retrying the same model) vs a permanent provider error
|
|
601
|
+
* (auth failure, quota exceeded, etc. — should fall back immediately).
|
|
602
|
+
*/
|
|
603
|
+
export function isTransientNetworkError(errorMsg: string): boolean {
|
|
604
|
+
if (!errorMsg) return false;
|
|
605
|
+
const hasNetworkSignal = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i.test(errorMsg);
|
|
606
|
+
const hasPermanentSignal = /auth|unauthorized|forbidden|invalid.*key|quota|billing/i.test(errorMsg);
|
|
607
|
+
return hasNetworkSignal && !hasPermanentSignal;
|
|
608
|
+
}
|
|
609
|
+
|
|
589
610
|
export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedModelConfig | undefined {
|
|
590
611
|
const prefs = loadEffectiveGSDPreferences();
|
|
591
612
|
if (!prefs?.preferences.models) return undefined;
|
|
@@ -773,6 +794,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
773
794
|
parallel: (base.parallel || override.parallel)
|
|
774
795
|
? { ...(base.parallel ?? {}), ...(override.parallel ?? {}) } as import("./types.js").ParallelConfig
|
|
775
796
|
: undefined,
|
|
797
|
+
verification_commands: mergeStringLists(base.verification_commands, override.verification_commands),
|
|
798
|
+
verification_auto_fix: override.verification_auto_fix ?? base.verification_auto_fix,
|
|
799
|
+
verification_max_retries: override.verification_max_retries ?? base.verification_max_retries,
|
|
776
800
|
};
|
|
777
801
|
}
|
|
778
802
|
|
|
@@ -1205,6 +1229,39 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
1205
1229
|
}
|
|
1206
1230
|
}
|
|
1207
1231
|
|
|
1232
|
+
// ─── Verification Preferences ───────────────────────────────────────────
|
|
1233
|
+
if (preferences.verification_commands !== undefined) {
|
|
1234
|
+
if (Array.isArray(preferences.verification_commands)) {
|
|
1235
|
+
const allStrings = preferences.verification_commands.every(
|
|
1236
|
+
(item: unknown) => typeof item === "string",
|
|
1237
|
+
);
|
|
1238
|
+
if (allStrings) {
|
|
1239
|
+
validated.verification_commands = preferences.verification_commands;
|
|
1240
|
+
} else {
|
|
1241
|
+
errors.push("verification_commands must be an array of strings");
|
|
1242
|
+
}
|
|
1243
|
+
} else {
|
|
1244
|
+
errors.push("verification_commands must be an array of strings");
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (preferences.verification_auto_fix !== undefined) {
|
|
1249
|
+
if (typeof preferences.verification_auto_fix === "boolean") {
|
|
1250
|
+
validated.verification_auto_fix = preferences.verification_auto_fix;
|
|
1251
|
+
} else {
|
|
1252
|
+
errors.push("verification_auto_fix must be a boolean");
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (preferences.verification_max_retries !== undefined) {
|
|
1257
|
+
const raw = preferences.verification_max_retries;
|
|
1258
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
|
|
1259
|
+
validated.verification_max_retries = Math.floor(raw);
|
|
1260
|
+
} else {
|
|
1261
|
+
errors.push("verification_max_retries must be a non-negative number");
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1208
1265
|
// ─── Git Preferences ───────────────────────────────────────────────────
|
|
1209
1266
|
if (preferences.git && typeof preferences.git === "object") {
|
|
1210
1267
|
const git: Record<string, unknown> = {};
|
|
@@ -1272,6 +1329,10 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
1272
1329
|
if (typeof g.commit_docs === "boolean") git.commit_docs = g.commit_docs;
|
|
1273
1330
|
else errors.push("git.commit_docs must be a boolean");
|
|
1274
1331
|
}
|
|
1332
|
+
if (g.manage_gitignore !== undefined) {
|
|
1333
|
+
if (typeof g.manage_gitignore === "boolean") git.manage_gitignore = g.manage_gitignore;
|
|
1334
|
+
else errors.push("git.manage_gitignore must be a boolean");
|
|
1335
|
+
}
|
|
1275
1336
|
if (g.worktree_post_create !== undefined) {
|
|
1276
1337
|
if (typeof g.worktree_post_create === "string" && g.worktree_post_create.trim()) {
|
|
1277
1338
|
git.worktree_post_create = g.worktree_post_create.trim();
|
|
@@ -38,15 +38,16 @@ Then:
|
|
|
38
38
|
- Preferred: use the `bg_shell` tool if available — it manages process lifecycle correctly without stream-inheritance issues
|
|
39
39
|
6. Verify must-haves are met by running concrete checks (tests, commands, observable behaviors)
|
|
40
40
|
7. Run the slice-level verification checks defined in the slice plan's Verification section. Track which pass. On the final task of the slice, all must pass before marking done. On intermediate tasks, partial passes are expected — note which ones pass in the summary.
|
|
41
|
-
8.
|
|
41
|
+
8. After the verification gate runs (you'll see gate results in stderr/notify output), populate the `## Verification Evidence` table in your task summary with the check results. Use the `formatEvidenceTable` format: one row per check with command, exit code, verdict (✅ pass / ❌ fail), and duration. If no verification commands were discovered, note that in the section.
|
|
42
|
+
9. If the task touches UI, browser flows, DOM behavior, or user-visible web state:
|
|
42
43
|
- exercise the real flow in the browser
|
|
43
44
|
- prefer `browser_batch` when the next few actions are obvious and sequential
|
|
44
45
|
- prefer `browser_assert` for explicit pass/fail verification of the intended outcome
|
|
45
46
|
- use `browser_diff` when an action's effect is ambiguous
|
|
46
47
|
- use console/network/dialog diagnostics when validating async, stateful, or failure-prone UI
|
|
47
48
|
- record verification in terms of explicit checks passed/failed, not only prose interpretation
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
10. If the task plan includes an Observability Impact section, verify those signals directly. Skip this step if the task plan omits the section.
|
|
50
|
+
11. **If execution is running long or verification fails:**
|
|
50
51
|
|
|
51
52
|
**Context budget:** You have approximately **{{verificationBudget}}** reserved for verification context. If you've used most of your context and haven't finished all steps, stop implementing and prioritize writing the task summary with clear notes on what's done and what remains. A partial summary that enables clean resumption is more valuable than one more half-finished step with no documentation. Never sacrifice summary quality for one more implementation step.
|
|
52
53
|
|
|
@@ -154,7 +154,7 @@ Templates showing the expected format for each artifact type are in:
|
|
|
154
154
|
|
|
155
155
|
**External facts:** Use `search-the-web` + `fetch_page`, or `search_and_read` for one-call extraction. Use `freshness` for recency. Never state current facts from training data without verification.
|
|
156
156
|
|
|
157
|
-
**Background processes:** Use `bg_shell` with `start` + `wait_for_ready` for servers, watchers, and daemons. Never use `bash` with `&` or `nohup` to background a process — the `bash` tool waits for stdout to close, so backgrounded children that inherit the file descriptors cause it to hang indefinitely. Never poll with `sleep`/retry loops — `wait_for_ready` exists for this. For status checks, use `digest` (~30 tokens), not `output` (~2000 tokens). Use `highlights` (~100 tokens) when you need significant lines only. Use `output` only when actively debugging.
|
|
157
|
+
**Background processes:** Use `bg_shell` with `start` + `wait_for_ready` for servers, watchers, and daemons. Never use `bash` with `&` or `nohup` to background a process — the `bash` tool waits for stdout to close, so backgrounded children that inherit the file descriptors cause it to hang indefinitely. Never poll with `sleep`/retry loops — `wait_for_ready` exists for this. For status checks, use `digest` (~30 tokens), not `output` (~2000 tokens). Use `highlights` (~100 tokens) when you need significant lines only. Use `output` only when actively debugging. Background processes are session-scoped by default; set `persist_across_sessions:true` only when you intentionally need them to survive a fresh session.
|
|
158
158
|
|
|
159
159
|
**One-shot commands:** Use `async_bash` for builds, tests, and installs. The result is pushed to you when the command exits — no polling needed. Use `await_job` to block on a specific job.
|
|
160
160
|
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Reports Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages .gsd/reports/ — the persistent progression log of HTML snapshots.
|
|
5
|
+
*
|
|
6
|
+
* Layout:
|
|
7
|
+
* .gsd/reports/
|
|
8
|
+
* reports.json lightweight metadata index (never re-parses HTML)
|
|
9
|
+
* index.html auto-regenerated on every new snapshot
|
|
10
|
+
* M001-20260101T120000.html per-milestone snapshot
|
|
11
|
+
* final-20260201T090000.html full-project final snapshot
|
|
12
|
+
*
|
|
13
|
+
* Auto-triggered: after each milestone completion (when auto_report: true).
|
|
14
|
+
* Manual: /gsd export --html
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
18
|
+
import { join, basename } from 'node:path';
|
|
19
|
+
import { gsdRoot } from './paths.js';
|
|
20
|
+
import { formatCost, formatTokenCount } from './metrics.js';
|
|
21
|
+
import { formatDuration } from './history.js';
|
|
22
|
+
|
|
23
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface ReportEntry {
|
|
26
|
+
/** Filename relative to the reports/ dir, e.g. "M001-20260101T120000.html" */
|
|
27
|
+
filename: string;
|
|
28
|
+
/** ISO timestamp when this report was generated */
|
|
29
|
+
generatedAt: string;
|
|
30
|
+
/** Milestone ID this snapshot covers, or "final" for a full-project snapshot */
|
|
31
|
+
milestoneId: string | 'final';
|
|
32
|
+
/** Milestone title at snapshot time */
|
|
33
|
+
milestoneTitle: string;
|
|
34
|
+
/** Human-readable label shown in the index */
|
|
35
|
+
label: string;
|
|
36
|
+
/** Snapshot kind */
|
|
37
|
+
kind: 'milestone' | 'manual' | 'final';
|
|
38
|
+
// Metrics at snapshot time — for the index progression view
|
|
39
|
+
totalCost: number;
|
|
40
|
+
totalTokens: number;
|
|
41
|
+
totalDuration: number;
|
|
42
|
+
doneSlices: number;
|
|
43
|
+
totalSlices: number;
|
|
44
|
+
doneMilestones: number;
|
|
45
|
+
totalMilestones: number;
|
|
46
|
+
phase: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ReportsIndex {
|
|
50
|
+
version: 1;
|
|
51
|
+
projectName: string;
|
|
52
|
+
projectPath: string;
|
|
53
|
+
gsdVersion: string;
|
|
54
|
+
entries: ReportEntry[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Paths ────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function reportsDir(basePath: string): string {
|
|
60
|
+
return join(gsdRoot(basePath), 'reports');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reportsIndexPath(basePath: string): string {
|
|
64
|
+
return join(reportsDir(basePath), 'reports.json');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function reportsHtmlIndexPath(basePath: string): string {
|
|
68
|
+
return join(reportsDir(basePath), 'index.html');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export function loadReportsIndex(basePath: string): ReportsIndex | null {
|
|
74
|
+
const p = reportsIndexPath(basePath);
|
|
75
|
+
if (!existsSync(p)) return null;
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(readFileSync(p, 'utf-8')) as ReportsIndex;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function saveReportsIndex(basePath: string, index: ReportsIndex): void {
|
|
84
|
+
const dir = reportsDir(basePath);
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
writeFileSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Write a report snapshot ──────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface WriteReportSnapshotArgs {
|
|
92
|
+
basePath: string;
|
|
93
|
+
html: string;
|
|
94
|
+
milestoneId: string | 'final';
|
|
95
|
+
milestoneTitle: string;
|
|
96
|
+
kind: 'milestone' | 'manual' | 'final';
|
|
97
|
+
projectName: string;
|
|
98
|
+
projectPath: string;
|
|
99
|
+
gsdVersion: string;
|
|
100
|
+
// metrics
|
|
101
|
+
totalCost: number;
|
|
102
|
+
totalTokens: number;
|
|
103
|
+
totalDuration: number;
|
|
104
|
+
doneSlices: number;
|
|
105
|
+
totalSlices: number;
|
|
106
|
+
doneMilestones: number;
|
|
107
|
+
totalMilestones: number;
|
|
108
|
+
phase: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Write a report snapshot to .gsd/reports/, update reports.json, regenerate index.html.
|
|
113
|
+
* Returns the path of the written report file.
|
|
114
|
+
*/
|
|
115
|
+
export function writeReportSnapshot(args: WriteReportSnapshotArgs): string {
|
|
116
|
+
const dir = reportsDir(args.basePath);
|
|
117
|
+
mkdirSync(dir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
120
|
+
const prefix = args.milestoneId === 'final' ? 'final' : args.milestoneId;
|
|
121
|
+
const filename = `${prefix}-${timestamp}.html`;
|
|
122
|
+
const filePath = join(dir, filename);
|
|
123
|
+
|
|
124
|
+
writeFileSync(filePath, args.html, 'utf-8');
|
|
125
|
+
|
|
126
|
+
// Load or init registry
|
|
127
|
+
const existing = loadReportsIndex(args.basePath);
|
|
128
|
+
const index: ReportsIndex = existing ?? {
|
|
129
|
+
version: 1,
|
|
130
|
+
projectName: args.projectName,
|
|
131
|
+
projectPath: args.projectPath,
|
|
132
|
+
gsdVersion: args.gsdVersion,
|
|
133
|
+
entries: [],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Keep metadata fresh
|
|
137
|
+
index.projectName = args.projectName;
|
|
138
|
+
index.projectPath = args.projectPath;
|
|
139
|
+
index.gsdVersion = args.gsdVersion;
|
|
140
|
+
|
|
141
|
+
const label = args.milestoneId === 'final'
|
|
142
|
+
? 'Final Report'
|
|
143
|
+
: `${args.milestoneId}: ${args.milestoneTitle}`;
|
|
144
|
+
|
|
145
|
+
const entry: ReportEntry = {
|
|
146
|
+
filename,
|
|
147
|
+
generatedAt: new Date().toISOString(),
|
|
148
|
+
milestoneId: args.milestoneId,
|
|
149
|
+
milestoneTitle: args.milestoneTitle,
|
|
150
|
+
label,
|
|
151
|
+
kind: args.kind,
|
|
152
|
+
totalCost: args.totalCost,
|
|
153
|
+
totalTokens: args.totalTokens,
|
|
154
|
+
totalDuration: args.totalDuration,
|
|
155
|
+
doneSlices: args.doneSlices,
|
|
156
|
+
totalSlices: args.totalSlices,
|
|
157
|
+
doneMilestones: args.doneMilestones,
|
|
158
|
+
totalMilestones: args.totalMilestones,
|
|
159
|
+
phase: args.phase,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
index.entries.push(entry);
|
|
163
|
+
saveReportsIndex(args.basePath, index);
|
|
164
|
+
regenerateHtmlIndex(args.basePath, index);
|
|
165
|
+
|
|
166
|
+
return filePath;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── HTML Index Generator ─────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export function regenerateHtmlIndex(basePath: string, index: ReportsIndex): void {
|
|
172
|
+
const html = buildIndexHtml(index);
|
|
173
|
+
writeFileSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildIndexHtml(index: ReportsIndex): string {
|
|
177
|
+
const { projectName, projectPath, gsdVersion, entries } = index;
|
|
178
|
+
const generated = new Date().toISOString();
|
|
179
|
+
|
|
180
|
+
// Sort oldest → newest for the progression timeline
|
|
181
|
+
const sorted = [...entries].sort(
|
|
182
|
+
(a, b) => new Date(a.generatedAt).getTime() - new Date(b.generatedAt).getTime()
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const latestEntry = sorted[sorted.length - 1];
|
|
186
|
+
const overallPct = latestEntry
|
|
187
|
+
? (latestEntry.totalSlices > 0
|
|
188
|
+
? Math.round((latestEntry.doneSlices / latestEntry.totalSlices) * 100)
|
|
189
|
+
: 0)
|
|
190
|
+
: 0;
|
|
191
|
+
|
|
192
|
+
// TOC: group by milestone
|
|
193
|
+
const milestoneGroups = new Map<string, ReportEntry[]>();
|
|
194
|
+
for (const e of sorted) {
|
|
195
|
+
const key = e.milestoneId;
|
|
196
|
+
const arr = milestoneGroups.get(key) ?? [];
|
|
197
|
+
arr.push(e);
|
|
198
|
+
milestoneGroups.set(key, arr);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const tocHtml = [...milestoneGroups.entries()].map(([mid, group]) => {
|
|
202
|
+
const links = group.map(e =>
|
|
203
|
+
`<li><a href="${esc(e.filename)}">${formatDateShort(e.generatedAt)}</a> <span class="toc-kind toc-${e.kind}">${e.kind}</span></li>`
|
|
204
|
+
).join('');
|
|
205
|
+
return `
|
|
206
|
+
<div class="toc-group">
|
|
207
|
+
<div class="toc-group-label">${esc(mid === 'final' ? 'Final' : mid)}</div>
|
|
208
|
+
<ul>${links}</ul>
|
|
209
|
+
</div>`;
|
|
210
|
+
}).join('');
|
|
211
|
+
|
|
212
|
+
// Progression cards
|
|
213
|
+
const cardHtml = sorted.map((e, i) => {
|
|
214
|
+
const pct = e.totalSlices > 0 ? Math.round((e.doneSlices / e.totalSlices) * 100) : 0;
|
|
215
|
+
const isLatest = i === sorted.length - 1;
|
|
216
|
+
|
|
217
|
+
// Delta vs previous
|
|
218
|
+
let deltaHtml = '';
|
|
219
|
+
if (i > 0) {
|
|
220
|
+
const prev = sorted[i - 1];
|
|
221
|
+
const dCost = e.totalCost - prev.totalCost;
|
|
222
|
+
const dSlices = e.doneSlices - prev.doneSlices;
|
|
223
|
+
const dMillestones = e.doneMilestones - prev.doneMilestones;
|
|
224
|
+
const parts: string[] = [];
|
|
225
|
+
if (dCost > 0) parts.push(`+${formatCost(dCost)}`);
|
|
226
|
+
if (dSlices > 0) parts.push(`+${dSlices} slice${dSlices !== 1 ? 's' : ''}`);
|
|
227
|
+
if (dMillestones > 0) parts.push(`+${dMillestones} milestone${dMillestones !== 1 ? 's' : ''}`);
|
|
228
|
+
if (parts.length > 0) {
|
|
229
|
+
deltaHtml = `<div class="card-delta">${parts.map(p => `<span>${esc(p)}</span>`).join('')}</div>`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return `
|
|
234
|
+
<a class="report-card${isLatest ? ' card-latest' : ''}" href="${esc(e.filename)}">
|
|
235
|
+
<div class="card-top">
|
|
236
|
+
<span class="card-label">${esc(e.label)}</span>
|
|
237
|
+
<span class="card-kind card-kind-${e.kind}">${e.kind}</span>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="card-date">${formatDateShort(e.generatedAt)}</div>
|
|
240
|
+
<div class="card-progress">
|
|
241
|
+
<div class="card-bar-track">
|
|
242
|
+
<div class="card-bar-fill" style="width:${pct}%"></div>
|
|
243
|
+
</div>
|
|
244
|
+
<span class="card-pct">${pct}%</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="card-stats">
|
|
247
|
+
<span>${esc(formatCost(e.totalCost))}</span>
|
|
248
|
+
<span>${esc(formatTokenCount(e.totalTokens))}</span>
|
|
249
|
+
<span>${esc(formatDuration(e.totalDuration))}</span>
|
|
250
|
+
<span>${e.doneSlices}/${e.totalSlices} slices</span>
|
|
251
|
+
</div>
|
|
252
|
+
${deltaHtml}
|
|
253
|
+
${isLatest ? '<div class="card-latest-badge">Latest</div>' : ''}
|
|
254
|
+
</a>`;
|
|
255
|
+
}).join('');
|
|
256
|
+
|
|
257
|
+
// Cost progression mini-chart (inline SVG sparkline)
|
|
258
|
+
const sparklineSvg = sorted.length > 1 ? buildCostSparkline(sorted) : '';
|
|
259
|
+
|
|
260
|
+
// Summary of latest state
|
|
261
|
+
const summaryHtml = latestEntry ? `
|
|
262
|
+
<div class="idx-summary">
|
|
263
|
+
<div class="idx-stat"><span class="idx-val">${formatCost(latestEntry.totalCost)}</span><span class="idx-lbl">Total Cost</span></div>
|
|
264
|
+
<div class="idx-stat"><span class="idx-val">${formatTokenCount(latestEntry.totalTokens)}</span><span class="idx-lbl">Total Tokens</span></div>
|
|
265
|
+
<div class="idx-stat"><span class="idx-val">${formatDuration(latestEntry.totalDuration)}</span><span class="idx-lbl">Duration</span></div>
|
|
266
|
+
<div class="idx-stat"><span class="idx-val">${latestEntry.doneSlices}/${latestEntry.totalSlices}</span><span class="idx-lbl">Slices</span></div>
|
|
267
|
+
<div class="idx-stat"><span class="idx-val">${latestEntry.doneMilestones}/${latestEntry.totalMilestones}</span><span class="idx-lbl">Milestones</span></div>
|
|
268
|
+
<div class="idx-stat"><span class="idx-val">${entries.length}</span><span class="idx-lbl">Reports</span></div>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="idx-progress">
|
|
271
|
+
<div class="idx-bar-track"><div class="idx-bar-fill" style="width:${overallPct}%"></div></div>
|
|
272
|
+
<span class="idx-pct">${overallPct}% complete</span>
|
|
273
|
+
</div>` : '<p class="empty">No reports generated yet.</p>';
|
|
274
|
+
|
|
275
|
+
return `<!DOCTYPE html>
|
|
276
|
+
<html lang="en">
|
|
277
|
+
<head>
|
|
278
|
+
<meta charset="UTF-8">
|
|
279
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
280
|
+
<title>GSD Reports — ${esc(projectName)}</title>
|
|
281
|
+
<style>${INDEX_CSS}</style>
|
|
282
|
+
</head>
|
|
283
|
+
<body>
|
|
284
|
+
<header>
|
|
285
|
+
<div class="hdr-inner">
|
|
286
|
+
<div class="branding">
|
|
287
|
+
<span class="logo">GSD</span>
|
|
288
|
+
<span class="ver">v${esc(gsdVersion)}</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="hdr-meta">
|
|
291
|
+
<h1>${esc(projectName)} <span class="hdr-subtitle">Reports</span></h1>
|
|
292
|
+
<span class="hdr-path">${esc(projectPath)}</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="hdr-right">
|
|
295
|
+
<span class="gen-lbl">Updated</span>
|
|
296
|
+
<span class="gen">${formatDateShort(generated)}</span>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</header>
|
|
300
|
+
|
|
301
|
+
<div class="layout">
|
|
302
|
+
<!-- Sidebar TOC -->
|
|
303
|
+
<aside class="sidebar">
|
|
304
|
+
<div class="sidebar-title">Reports</div>
|
|
305
|
+
${sorted.length > 0 ? tocHtml : '<p class="empty">No reports yet.</p>'}
|
|
306
|
+
</aside>
|
|
307
|
+
|
|
308
|
+
<!-- Main content -->
|
|
309
|
+
<main>
|
|
310
|
+
<section class="idx-overview">
|
|
311
|
+
<h2>Project Overview</h2>
|
|
312
|
+
${summaryHtml}
|
|
313
|
+
${sparklineSvg ? `<div class="sparkline-wrap"><h3>Cost Progression</h3>${sparklineSvg}</div>` : ''}
|
|
314
|
+
</section>
|
|
315
|
+
|
|
316
|
+
<section class="idx-cards">
|
|
317
|
+
<h2>Progression <span class="sec-count">${entries.length}</span></h2>
|
|
318
|
+
${sorted.length > 0
|
|
319
|
+
? `<div class="cards-grid">${cardHtml}</div>`
|
|
320
|
+
: '<p class="empty">No reports generated yet. Run <code>/gsd export --html</code> or enable <code>auto_report: true</code>.</p>'}
|
|
321
|
+
</section>
|
|
322
|
+
</main>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<footer>
|
|
326
|
+
<div class="ftr-inner">
|
|
327
|
+
<span class="ftr-brand">GSD v${esc(gsdVersion)}</span>
|
|
328
|
+
<span class="ftr-sep">—</span>
|
|
329
|
+
<span>${esc(projectName)}</span>
|
|
330
|
+
<span class="ftr-sep">—</span>
|
|
331
|
+
<span>${esc(projectPath)}</span>
|
|
332
|
+
<span class="ftr-sep">—</span>
|
|
333
|
+
<span>Updated ${formatDateShort(generated)}</span>
|
|
334
|
+
</div>
|
|
335
|
+
</footer>
|
|
336
|
+
</body>
|
|
337
|
+
</html>`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Cost sparkline (inline SVG) ──────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
function buildCostSparkline(entries: ReportEntry[]): string {
|
|
343
|
+
const costs = entries.map(e => e.totalCost);
|
|
344
|
+
const maxCost = Math.max(...costs, 0.001);
|
|
345
|
+
const W = 600, H = 60, PAD = 12;
|
|
346
|
+
const xStep = entries.length > 1 ? (W - PAD * 2) / (entries.length - 1) : W - PAD * 2;
|
|
347
|
+
|
|
348
|
+
const points = costs.map((c, i) => {
|
|
349
|
+
const x = PAD + i * xStep;
|
|
350
|
+
const y = PAD + (1 - c / maxCost) * (H - PAD * 2);
|
|
351
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
352
|
+
}).join(' ');
|
|
353
|
+
|
|
354
|
+
const dots = costs.map((c, i) => {
|
|
355
|
+
const x = PAD + i * xStep;
|
|
356
|
+
const y = PAD + (1 - c / maxCost) * (H - PAD * 2);
|
|
357
|
+
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3" class="spark-dot">
|
|
358
|
+
<title>${esc(entries[i].label)} — ${formatCost(c)}</title>
|
|
359
|
+
</circle>`;
|
|
360
|
+
}).join('');
|
|
361
|
+
|
|
362
|
+
// Labels at start and end
|
|
363
|
+
const startLabel = formatCost(costs[0]);
|
|
364
|
+
const endLabel = formatCost(costs[costs.length - 1]);
|
|
365
|
+
|
|
366
|
+
return `
|
|
367
|
+
<div class="sparkline">
|
|
368
|
+
<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" class="spark-svg">
|
|
369
|
+
<polyline points="${esc(points)}" class="spark-line" fill="none"/>
|
|
370
|
+
${dots}
|
|
371
|
+
<text x="${PAD}" y="${H - 2}" class="spark-lbl">${esc(startLabel)}</text>
|
|
372
|
+
<text x="${W - PAD}" y="${H - 2}" text-anchor="end" class="spark-lbl">${esc(endLabel)}</text>
|
|
373
|
+
</svg>
|
|
374
|
+
<div class="spark-axis">
|
|
375
|
+
${entries.map((e, i) => {
|
|
376
|
+
const x = (PAD + i * xStep) / W * 100;
|
|
377
|
+
return `<span class="spark-tick" style="left:${x.toFixed(1)}%" title="${esc(e.generatedAt)}">${esc(e.milestoneId === 'final' ? 'final' : e.milestoneId)}</span>`;
|
|
378
|
+
}).join('')}
|
|
379
|
+
</div>
|
|
380
|
+
</div>`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
function formatDateShort(iso: string): string {
|
|
386
|
+
try {
|
|
387
|
+
const d = new Date(iso);
|
|
388
|
+
return d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
389
|
+
} catch { return iso; }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function esc(s: string | number | undefined | null): string {
|
|
393
|
+
if (s == null) return '';
|
|
394
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ─── Index CSS ────────────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
const INDEX_CSS = `
|
|
400
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
401
|
+
:root{
|
|
402
|
+
--bg-0:#0f1115;--bg-1:#16181d;--bg-2:#1e2028;--bg-3:#272a33;
|
|
403
|
+
--border-1:#2b2e38;--border-2:#3b3f4c;
|
|
404
|
+
--text-0:#ededef;--text-1:#a1a1aa;--text-2:#71717a;
|
|
405
|
+
--accent:#5e6ad2;--accent-subtle:rgba(94,106,210,.12);
|
|
406
|
+
--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
|
407
|
+
--mono:'JetBrains Mono','Fira Code',ui-monospace,monospace;
|
|
408
|
+
}
|
|
409
|
+
html{font-size:13px}
|
|
410
|
+
body{background:var(--bg-0);color:var(--text-0);font-family:var(--font);line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
411
|
+
a{color:var(--accent);text-decoration:none}
|
|
412
|
+
a:hover{text-decoration:underline}
|
|
413
|
+
h2{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-1);margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border-1)}
|
|
414
|
+
h3{font-size:13px;font-weight:600;color:var(--text-1);margin:16px 0 8px}
|
|
415
|
+
code{font-family:var(--mono);font-size:12px;background:var(--bg-3);padding:1px 5px;border-radius:3px}
|
|
416
|
+
.empty{color:var(--text-2);font-size:13px;padding:8px 0}
|
|
417
|
+
.count{font-size:11px;font-weight:500;color:var(--text-2);background:var(--bg-3);border-radius:3px;padding:1px 6px}
|
|
418
|
+
|
|
419
|
+
/* Header */
|
|
420
|
+
header{background:var(--bg-1);border-bottom:1px solid var(--border-1);padding:12px 32px;position:sticky;top:0;z-index:100}
|
|
421
|
+
.hdr-inner{display:flex;align-items:center;gap:16px;max-width:1280px;margin:0 auto}
|
|
422
|
+
.branding{display:flex;align-items:baseline;gap:6px;flex-shrink:0}
|
|
423
|
+
.logo{font-size:18px;font-weight:800;letter-spacing:-.5px;color:var(--text-0)}
|
|
424
|
+
.ver{font-size:10px;color:var(--text-2);font-family:var(--mono)}
|
|
425
|
+
.hdr-meta{flex:1;min-width:0}
|
|
426
|
+
.hdr-meta h1{font-size:15px;font-weight:600}
|
|
427
|
+
.hdr-subtitle{color:var(--text-2);font-weight:400;font-size:13px;margin-left:4px}
|
|
428
|
+
.hdr-path{font-size:11px;color:var(--text-2);font-family:var(--mono);display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
429
|
+
.hdr-right{text-align:right;flex-shrink:0}
|
|
430
|
+
.gen-lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px;display:block}
|
|
431
|
+
.gen{font-size:11px;color:var(--text-1)}
|
|
432
|
+
|
|
433
|
+
/* Layout */
|
|
434
|
+
.layout{display:grid;grid-template-columns:200px 1fr;gap:0;max-width:1280px;margin:0 auto;min-height:calc(100vh - 120px)}
|
|
435
|
+
|
|
436
|
+
/* Sidebar */
|
|
437
|
+
.sidebar{background:var(--bg-1);border-right:1px solid var(--border-1);padding:20px 14px;position:sticky;top:52px;height:calc(100vh - 52px);overflow-y:auto}
|
|
438
|
+
.sidebar-title{font-size:10px;font-weight:600;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px}
|
|
439
|
+
.toc-group{margin-bottom:14px}
|
|
440
|
+
.toc-group-label{font-size:11px;font-weight:600;color:var(--text-1);margin-bottom:3px;font-family:var(--mono)}
|
|
441
|
+
.toc-group ul{list-style:none;display:flex;flex-direction:column;gap:1px}
|
|
442
|
+
.toc-group li{display:flex;align-items:center;gap:6px}
|
|
443
|
+
.toc-group a{font-size:11px;color:var(--text-2);padding:2px 4px;border-radius:3px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
444
|
+
.toc-group a:hover{background:var(--bg-2);color:var(--text-0);text-decoration:none}
|
|
445
|
+
.toc-kind{font-size:9px;color:var(--text-2);font-family:var(--mono);flex-shrink:0}
|
|
446
|
+
|
|
447
|
+
/* Main */
|
|
448
|
+
main{padding:28px;display:flex;flex-direction:column;gap:40px}
|
|
449
|
+
|
|
450
|
+
/* Overview */
|
|
451
|
+
.idx-summary{display:flex;flex-wrap:wrap;gap:1px;background:var(--border-1);border:1px solid var(--border-1);border-radius:4px;overflow:hidden;margin-bottom:16px}
|
|
452
|
+
.idx-stat{background:var(--bg-1);padding:10px 16px;display:flex;flex-direction:column;gap:2px;min-width:100px;flex:1}
|
|
453
|
+
.idx-val{font-size:18px;font-weight:600;color:var(--text-0);font-variant-numeric:tabular-nums}
|
|
454
|
+
.idx-lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.4px}
|
|
455
|
+
.idx-progress{display:flex;align-items:center;gap:10px;margin-top:10px}
|
|
456
|
+
.idx-bar-track{flex:1;height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden}
|
|
457
|
+
.idx-bar-fill{height:100%;background:var(--accent);border-radius:2px}
|
|
458
|
+
.idx-pct{font-size:12px;font-weight:600;color:var(--text-1);min-width:40px;text-align:right}
|
|
459
|
+
|
|
460
|
+
/* Sparkline */
|
|
461
|
+
.sparkline-wrap{margin-top:20px}
|
|
462
|
+
.sparkline{position:relative}
|
|
463
|
+
.spark-svg{display:block;background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;overflow:visible;max-width:100%}
|
|
464
|
+
.spark-line{stroke:var(--accent);stroke-width:1.5;fill:none}
|
|
465
|
+
.spark-dot{fill:var(--accent);stroke:var(--bg-1);stroke-width:2;cursor:pointer}
|
|
466
|
+
.spark-dot:hover{r:4;fill:var(--text-0)}
|
|
467
|
+
.spark-lbl{font-size:10px;fill:var(--text-2);font-family:var(--mono)}
|
|
468
|
+
.spark-axis{display:flex;position:relative;height:18px;margin-top:2px}
|
|
469
|
+
.spark-tick{position:absolute;transform:translateX(-50%);font-size:9px;color:var(--text-2);font-family:var(--mono);white-space:nowrap}
|
|
470
|
+
|
|
471
|
+
/* Report cards */
|
|
472
|
+
.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
|
|
473
|
+
.report-card{
|
|
474
|
+
display:flex;flex-direction:column;gap:6px;
|
|
475
|
+
background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;
|
|
476
|
+
padding:14px;text-decoration:none;color:var(--text-0);
|
|
477
|
+
transition:border-color .12s;
|
|
478
|
+
}
|
|
479
|
+
.report-card:hover{border-color:var(--accent);text-decoration:none}
|
|
480
|
+
.card-latest{border-color:var(--accent)}
|
|
481
|
+
.card-top{display:flex;align-items:center;gap:8px}
|
|
482
|
+
.card-label{flex:1;font-weight:500;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
483
|
+
.card-kind{font-size:10px;color:var(--text-2);font-family:var(--mono);flex-shrink:0}
|
|
484
|
+
.card-date{font-size:11px;color:var(--text-2)}
|
|
485
|
+
.card-progress{display:flex;align-items:center;gap:6px}
|
|
486
|
+
.card-bar-track{flex:1;height:3px;background:var(--bg-3);border-radius:2px;overflow:hidden}
|
|
487
|
+
.card-bar-fill{height:100%;background:var(--accent);border-radius:2px}
|
|
488
|
+
.card-pct{font-size:11px;color:var(--text-2);min-width:30px;text-align:right}
|
|
489
|
+
.card-stats{display:flex;gap:8px;flex-wrap:wrap}
|
|
490
|
+
.card-stats span{font-size:11px;color:var(--text-2);font-variant-numeric:tabular-nums}
|
|
491
|
+
.card-delta{display:flex;gap:4px;flex-wrap:wrap}
|
|
492
|
+
.card-delta span{font-size:10px;color:var(--text-1);font-family:var(--mono)}
|
|
493
|
+
.card-latest-badge{display:none}
|
|
494
|
+
|
|
495
|
+
/* Footer */
|
|
496
|
+
footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
|
497
|
+
.ftr-inner{display:flex;align-items:center;gap:6px;justify-content:center;font-size:11px;color:var(--text-2)}
|
|
498
|
+
.ftr-sep{color:var(--border-2)}
|
|
499
|
+
|
|
500
|
+
@media(max-width:768px){
|
|
501
|
+
.layout{grid-template-columns:1fr}
|
|
502
|
+
.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--border-1)}
|
|
503
|
+
}
|
|
504
|
+
@media print{
|
|
505
|
+
.sidebar{display:none}
|
|
506
|
+
header{position:static}
|
|
507
|
+
body{background:#fff;color:#1a1a1a}
|
|
508
|
+
:root{--bg-0:#fff;--bg-1:#fafafa;--bg-2:#f5f5f5;--bg-3:#ebebeb;--border-1:#e5e5e5;--border-2:#d4d4d4;--text-0:#1a1a1a;--text-1:#525252;--text-2:#a3a3a3;--accent:#4f46e5}
|
|
509
|
+
}
|
|
510
|
+
`;
|
|
@@ -41,7 +41,7 @@ export function expandDependencies(deps: string[]): string[] {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function extractSlicesSection(content: string): string {
|
|
44
|
-
const headingMatch = /^## Slices\
|
|
44
|
+
const headingMatch = /^## Slices\b.*$/m.exec(content);
|
|
45
45
|
if (!headingMatch || headingMatch.index == null) return "";
|
|
46
46
|
|
|
47
47
|
const start = headingMatch.index + headingMatch[0].length;
|