@overscore/cli 0.6.1 → 0.8.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/dist/index.js CHANGED
@@ -629,6 +629,394 @@ function collectFiles(dir, prefix) {
629
629
  }
630
630
  return files;
631
631
  }
632
+ // ── Analysis commands ──────────────────────────────────────────────
633
+ const HUB_URL = "https://overscore.dev";
634
+ function slugify(input) {
635
+ return input
636
+ .toLowerCase()
637
+ .replace(/[^a-z0-9]+/g, "-")
638
+ .replace(/^-+|-+$/g, "")
639
+ .slice(0, 80);
640
+ }
641
+ /**
642
+ * Resolve API config for analysis commands.
643
+ *
644
+ * Priority:
645
+ * 1. Local .env in cwd (e.g. running from inside an existing dashboard project)
646
+ * 2. ~/.overscore/config (set by install-skills or a previous analysis new run)
647
+ * 3. Interactive prompt (asks for API key, project slug)
648
+ *
649
+ * Analysis commands work from anywhere — they don't require being inside a project folder.
650
+ */
651
+ async function loadAnalysisConfig() {
652
+ // (1) Local .env
653
+ const envPath = path.resolve(process.cwd(), ".env");
654
+ if (fs.existsSync(envPath)) {
655
+ const env = parseEnv(fs.readFileSync(envPath, "utf-8"));
656
+ const apiUrl = env.VITE_OVERSCORE_API_URL;
657
+ const apiKey = env.VITE_OVERSCORE_API_KEY;
658
+ const projectSlug = env.VITE_OVERSCORE_PROJECT_SLUG;
659
+ if (apiUrl && apiKey && projectSlug) {
660
+ return { apiUrl, apiKey, projectSlug };
661
+ }
662
+ }
663
+ // (2) ~/.overscore/config
664
+ const configDir = path.join(process.env.HOME || "", ".overscore");
665
+ const configPath = path.join(configDir, "config");
666
+ if (fs.existsSync(configPath)) {
667
+ const cfg = parseEnv(fs.readFileSync(configPath, "utf-8"));
668
+ if (cfg.OVERSCORE_API_KEY && cfg.OVERSCORE_PROJECT_SLUG) {
669
+ return {
670
+ apiUrl: cfg.OVERSCORE_API_URL || `${HUB_URL}/api`,
671
+ apiKey: cfg.OVERSCORE_API_KEY,
672
+ projectSlug: cfg.OVERSCORE_PROJECT_SLUG,
673
+ };
674
+ }
675
+ }
676
+ // (3) Interactive
677
+ console.log("\n No Overscore config found. Let's set one up.\n");
678
+ const apiKey = await prompt("API key (from the Hub):");
679
+ if (!apiKey || !apiKey.startsWith("os_")) {
680
+ console.error("\n Error: API key should start with os_\n");
681
+ process.exit(1);
682
+ }
683
+ const projectSlug = await prompt("Project slug:");
684
+ if (!projectSlug) {
685
+ console.error("\n Error: Project slug is required\n");
686
+ process.exit(1);
687
+ }
688
+ // Save for next time
689
+ fs.mkdirSync(configDir, { recursive: true });
690
+ fs.writeFileSync(configPath, `OVERSCORE_API_URL=${HUB_URL}/api\nOVERSCORE_API_KEY=${apiKey}\nOVERSCORE_PROJECT_SLUG=${projectSlug}\n`, { mode: 0o600 });
691
+ console.log(`\n Saved to ~/.overscore/config\n`);
692
+ return { apiUrl: `${HUB_URL}/api`, apiKey, projectSlug };
693
+ }
694
+ async function analysisApiRequest(cfg, method, urlPath, body) {
695
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
696
+ const url = `${baseUrl}${urlPath}`;
697
+ const headers = {
698
+ Authorization: `Bearer ${cfg.apiKey}`,
699
+ };
700
+ if (body !== undefined)
701
+ headers["Content-Type"] = "application/json";
702
+ const res = await fetch(url, {
703
+ method,
704
+ headers,
705
+ body: body !== undefined ? JSON.stringify(body) : undefined,
706
+ });
707
+ const data = await res.json();
708
+ if (!res.ok) {
709
+ console.error(`\n Error: ${data.error || `HTTP ${res.status}`}\n`);
710
+ process.exit(1);
711
+ }
712
+ return data;
713
+ }
714
+ /**
715
+ * Recursively copies a template directory to a target directory, substituting
716
+ * `{{KEY}}` placeholders in text files. Mirrors directory structure.
717
+ *
718
+ * Substitutions are applied to .md, .html, .json, .yml, .yaml files. Other
719
+ * file types are copied byte-for-byte.
720
+ */
721
+ function scaffoldFromTemplate(templateDir, targetDir, substitutions) {
722
+ const TEXT_EXTENSIONS = new Set([".md", ".html", ".json", ".yml", ".yaml"]);
723
+ function substitute(content) {
724
+ let out = content;
725
+ for (const [key, value] of Object.entries(substitutions)) {
726
+ out = out.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
727
+ }
728
+ return out;
729
+ }
730
+ function walk(srcDir, destDir) {
731
+ fs.mkdirSync(destDir, { recursive: true });
732
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
733
+ const srcPath = path.join(srcDir, entry.name);
734
+ const destPath = path.join(destDir, entry.name);
735
+ if (entry.isDirectory()) {
736
+ walk(srcPath, destPath);
737
+ }
738
+ else if (entry.isFile()) {
739
+ const ext = path.extname(entry.name).toLowerCase();
740
+ if (TEXT_EXTENSIONS.has(ext)) {
741
+ const content = fs.readFileSync(srcPath, "utf8");
742
+ fs.writeFileSync(destPath, substitute(content));
743
+ }
744
+ else {
745
+ fs.copyFileSync(srcPath, destPath);
746
+ }
747
+ }
748
+ }
749
+ }
750
+ walk(templateDir, targetDir);
751
+ }
752
+ async function analysisNew(title) {
753
+ if (!title) {
754
+ console.error('\n Usage: npx @overscore/cli analysis new "<title>"\n');
755
+ process.exit(1);
756
+ }
757
+ const cfg = await loadAnalysisConfig();
758
+ console.log(`\n Creating analysis: "${title}"\n`);
759
+ const data = (await analysisApiRequest(cfg, "POST", "/api/analyses", {
760
+ title,
761
+ }));
762
+ // Scaffold the local folder
763
+ const folderName = data.slug;
764
+ const targetDir = path.resolve(process.cwd(), folderName);
765
+ if (fs.existsSync(targetDir)) {
766
+ console.error(`\n Error: Directory "${folderName}" already exists.\n`);
767
+ process.exit(1);
768
+ }
769
+ // Scaffold from the template tree shipped with the package.
770
+ // __dirname after build is dist/, so the templates dir is one level up.
771
+ const distDir = path.dirname(new URL(import.meta.url).pathname);
772
+ const pkgRoot = path.resolve(distDir, "..");
773
+ const templateDir = path.join(pkgRoot, "templates", "analysis");
774
+ if (!fs.existsSync(templateDir)) {
775
+ console.error(`\n Error: Bundled analysis template not found at ${templateDir}\n`);
776
+ process.exit(1);
777
+ }
778
+ fs.mkdirSync(targetDir, { recursive: true });
779
+ fs.mkdirSync(path.join(targetDir, ".overscore"), { recursive: true });
780
+ // Write analysis.json metadata (not part of the template — has dynamic id)
781
+ fs.writeFileSync(path.join(targetDir, ".overscore", "analysis.json"), JSON.stringify({
782
+ id: data.id,
783
+ slug: data.slug,
784
+ title: data.title,
785
+ project_slug: data.project_slug,
786
+ created_at: new Date().toISOString(),
787
+ }, null, 2));
788
+ // Walk the template tree, substituting placeholders
789
+ const today = new Date().toISOString().slice(0, 10);
790
+ scaffoldFromTemplate(templateDir, targetDir, {
791
+ TITLE: data.title,
792
+ SLUG: data.slug,
793
+ PROJECT_SLUG: data.project_slug,
794
+ DATE: today,
795
+ });
796
+ // Pull project's company-context.md and any project-level rule overrides
797
+ // (best-effort, non-fatal — the template defaults are still in place if
798
+ // the network is down or the hub is unreachable).
799
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
800
+ const authHeader = { Authorization: `Bearer ${cfg.apiKey}` };
801
+ // Company context
802
+ try {
803
+ const ctxRes = await fetch(`${baseUrl}/api/projects/${data.project_slug}/context`, { headers: authHeader });
804
+ if (ctxRes.ok) {
805
+ const ctxData = (await ctxRes.json());
806
+ if (ctxData.context_md) {
807
+ fs.writeFileSync(path.join(targetDir, ".claude", "rules", "company-context.md"), ctxData.context_md);
808
+ }
809
+ }
810
+ }
811
+ catch {
812
+ // Non-fatal
813
+ }
814
+ // SQL style override — if the project has a custom version, replace the
815
+ // template default. If null/missing, the template default stays in place.
816
+ try {
817
+ const sqlRes = await fetch(`${baseUrl}/api/projects/${data.project_slug}/sql-style`, { headers: authHeader });
818
+ if (sqlRes.ok) {
819
+ const sqlData = (await sqlRes.json());
820
+ if (sqlData.sql_style_md) {
821
+ fs.writeFileSync(path.join(targetDir, ".claude", "rules", "sql-style.md"), sqlData.sql_style_md);
822
+ }
823
+ }
824
+ }
825
+ catch {
826
+ // Non-fatal
827
+ }
828
+ console.log(` Analysis created: ${data.title}
829
+ Slug: ${data.slug}
830
+ Project: ${data.project_slug}
831
+ Folder: ./${folderName}/
832
+
833
+ Next steps:
834
+
835
+ cd ${folderName}
836
+ code .
837
+ claude
838
+
839
+ Then ask Claude to do the analysis. When the report.html looks good:
840
+
841
+ npx @overscore/cli analysis publish
842
+
843
+ The analysis will go live at:
844
+ https://${data.project_slug}.overscore.dev/analysis/${data.slug}
845
+ `);
846
+ }
847
+ function loadAnalysisMetadata() {
848
+ const metaPath = path.resolve(process.cwd(), ".overscore", "analysis.json");
849
+ if (!fs.existsSync(metaPath)) {
850
+ console.error("\n Error: Not in an analysis folder. Run 'npx @overscore/cli analysis new \"<title>\"' first.\n");
851
+ process.exit(1);
852
+ }
853
+ return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
854
+ }
855
+ const ANALYSIS_PUBLISH_EXCLUDE = new Set([
856
+ "node_modules",
857
+ ".git",
858
+ ".DS_Store",
859
+ ".env",
860
+ ]);
861
+ function collectAnalysisFiles(dir, prefix) {
862
+ const files = [];
863
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
864
+ if (ANALYSIS_PUBLISH_EXCLUDE.has(entry.name))
865
+ continue;
866
+ if (entry.name.startsWith(".env"))
867
+ continue;
868
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
869
+ if (entry.isDirectory()) {
870
+ files.push(...collectAnalysisFiles(path.join(dir, entry.name), relativePath));
871
+ }
872
+ else {
873
+ files.push(relativePath);
874
+ }
875
+ }
876
+ return files;
877
+ }
878
+ async function analysisPublish() {
879
+ const meta = loadAnalysisMetadata();
880
+ const cfg = await loadAnalysisConfig();
881
+ const reportPath = path.resolve(process.cwd(), "report.html");
882
+ if (!fs.existsSync(reportPath)) {
883
+ console.error("\n Error: report.html not found in this folder. Generate it before publishing.\n");
884
+ process.exit(1);
885
+ }
886
+ console.log(`\n Publishing analysis: ${meta.title}\n`);
887
+ const files = collectAnalysisFiles(process.cwd(), "");
888
+ console.log(` Bundling ${files.length} files...`);
889
+ const formData = new FormData();
890
+ formData.append("analysis_slug", meta.slug);
891
+ for (const file of files) {
892
+ const filePath = path.join(process.cwd(), file);
893
+ const content = fs.readFileSync(filePath);
894
+ const blob = new Blob([content]);
895
+ formData.append(`file:${file}`, blob, file);
896
+ }
897
+ console.log(" Uploading...");
898
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
899
+ const res = await fetch(`${baseUrl}/api/analyses/publish`, {
900
+ method: "POST",
901
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
902
+ body: formData,
903
+ });
904
+ const data = (await res.json());
905
+ if (!res.ok) {
906
+ console.error(`\n Publish failed: ${data.error}\n`);
907
+ process.exit(1);
908
+ }
909
+ console.log(`
910
+ Published successfully!
911
+
912
+ ${data.file_count} files uploaded
913
+ URL: ${data.url}
914
+ `);
915
+ }
916
+ async function analysisPull(slug) {
917
+ if (!slug) {
918
+ console.error("\n Usage: npx @overscore/cli analysis pull <slug>\n");
919
+ process.exit(1);
920
+ }
921
+ const cfg = await loadAnalysisConfig();
922
+ console.log(`\n Pulling analysis: ${slug}...`);
923
+ const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
924
+ const res = await fetch(`${baseUrl}/api/analyses/${slug}/source`, {
925
+ headers: { Authorization: `Bearer ${cfg.apiKey}` },
926
+ });
927
+ if (!res.ok) {
928
+ let errorMsg = `HTTP ${res.status}`;
929
+ try {
930
+ const body = await res.json();
931
+ errorMsg = body.error || errorMsg;
932
+ }
933
+ catch {
934
+ // not JSON
935
+ }
936
+ console.error(`\n Error: ${errorMsg}\n`);
937
+ process.exit(1);
938
+ }
939
+ const analysisSlug = res.headers.get("X-OS-Analysis-Slug") || slug;
940
+ const analysisTitle = res.headers.get("X-OS-Analysis-Title") || slug;
941
+ const projectSlug = res.headers.get("X-OS-Project-Slug") || cfg.projectSlug;
942
+ const zipBuffer = Buffer.from(await res.arrayBuffer());
943
+ const targetDir = path.resolve(process.cwd(), analysisSlug);
944
+ if (fs.existsSync(targetDir)) {
945
+ const yes = await confirm(`Directory "${analysisSlug}" already exists. Overwrite?`);
946
+ if (!yes) {
947
+ console.log(" Cancelled.\n");
948
+ return;
949
+ }
950
+ fs.rmSync(targetDir, { recursive: true, force: true });
951
+ }
952
+ fs.mkdirSync(targetDir, { recursive: true });
953
+ await extractZip(zipBuffer, targetDir);
954
+ // Make sure analysis.json reflects the current project (in case the user
955
+ // pulled into a different machine where their default project is different)
956
+ const metaPath = path.join(targetDir, ".overscore", "analysis.json");
957
+ if (fs.existsSync(metaPath)) {
958
+ try {
959
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
960
+ meta.project_slug = projectSlug;
961
+ meta.slug = analysisSlug;
962
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
963
+ }
964
+ catch {
965
+ // Non-fatal
966
+ }
967
+ }
968
+ console.log(`
969
+ Pulled "${analysisTitle}"
970
+
971
+ Next steps:
972
+
973
+ cd ${analysisSlug}
974
+ code .
975
+ claude
976
+
977
+ Claude will read .claude/CLAUDE.md and analysis-log.md to know what was done.
978
+
979
+ When you're ready to republish:
980
+
981
+ npx @overscore/cli analysis publish
982
+ `);
983
+ }
984
+ // ── install-skills ──────────────────────────────────────────────────
985
+ async function installSkills() {
986
+ // Skills ship with the npm package at <pkg-root>/skills/
987
+ // __dirname after build is dist/, so the skills dir is one level up.
988
+ const distDir = path.dirname(new URL(import.meta.url).pathname);
989
+ const pkgRoot = path.resolve(distDir, "..");
990
+ const skillsSrc = path.join(pkgRoot, "skills");
991
+ if (!fs.existsSync(skillsSrc)) {
992
+ console.error(`\n Error: Bundled skills directory not found at ${skillsSrc}\n`);
993
+ process.exit(1);
994
+ }
995
+ const homeSkills = path.join(process.env.HOME || "", ".claude", "skills");
996
+ fs.mkdirSync(homeSkills, { recursive: true });
997
+ const installed = [];
998
+ for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
999
+ if (!entry.isDirectory())
1000
+ continue;
1001
+ const srcDir = path.join(skillsSrc, entry.name);
1002
+ const destDir = path.join(homeSkills, entry.name);
1003
+ fs.rmSync(destDir, { recursive: true, force: true });
1004
+ fs.mkdirSync(destDir, { recursive: true });
1005
+ for (const f of fs.readdirSync(srcDir)) {
1006
+ fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
1007
+ }
1008
+ installed.push(entry.name);
1009
+ }
1010
+ console.log(`
1011
+ Installed ${installed.length} skill${installed.length === 1 ? "" : "s"} into ~/.claude/skills/:
1012
+ ${installed.map((s) => ` - ${s}`).join("\n")}
1013
+
1014
+ Claude Code will auto-detect and use these skills in any session,
1015
+ including when you switch between Overscore dashboards and analyses.
1016
+
1017
+ Re-run this command anytime to update.
1018
+ `);
1019
+ }
632
1020
  // ── Routing ─────────────────────────────────────────────────────────
633
1021
  const command = process.argv[2];
634
1022
  const subcommand = process.argv[3];
@@ -644,6 +1032,35 @@ else if (command === "versions") {
644
1032
  else if (command === "list") {
645
1033
  listDashboards();
646
1034
  }
1035
+ else if (command === "install-skills") {
1036
+ installSkills();
1037
+ }
1038
+ else if (command === "analysis") {
1039
+ if (subcommand === "new") {
1040
+ analysisNew(process.argv[4]);
1041
+ }
1042
+ else if (subcommand === "publish") {
1043
+ analysisPublish();
1044
+ }
1045
+ else if (subcommand === "pull") {
1046
+ analysisPull(process.argv[4]);
1047
+ }
1048
+ else {
1049
+ console.log(`
1050
+ overscore analysis — Manage Overscore Analyses (frozen investigations)
1051
+
1052
+ Commands:
1053
+ new "<title>" Create a new analysis in the current directory
1054
+ publish Upload report.html + bundle from the current analysis folder
1055
+ pull <slug> Re-download an existing analysis bundle to continue working
1056
+
1057
+ Examples:
1058
+ npx @overscore/cli analysis new "Why did Q1 churn spike"
1059
+ npx @overscore/cli analysis publish
1060
+ npx @overscore/cli analysis pull why-did-q1-churn-spike
1061
+ `);
1062
+ }
1063
+ }
647
1064
  else if (command === "query") {
648
1065
  if (subcommand === "list") {
649
1066
  queryList();
@@ -694,7 +1111,9 @@ else {
694
1111
  deploy Build and deploy the dashboard
695
1112
  pull <slug> Pull source code from the hub
696
1113
  versions List deploy history
697
- query <subcommand> Manage dashboard queries
1114
+ query <subcommand> Manage dashboard queries
1115
+ analysis <subcommand> Manage analyses (frozen investigations)
1116
+ install-skills Install the Overscore skill library into ~/.claude/skills/
698
1117
 
699
1118
  Usage:
700
1119
  npx @overscore/cli list
@@ -707,5 +1126,9 @@ else {
707
1126
  npx @overscore/cli query update <name> "<sql>"
708
1127
  npx @overscore/cli query remove <name>
709
1128
  npx @overscore/cli query test "<sql>"
1129
+ npx @overscore/cli analysis new "<title>"
1130
+ npx @overscore/cli analysis publish
1131
+ npx @overscore/cli analysis pull <slug>
1132
+ npx @overscore/cli install-skills
710
1133
  `);
711
1134
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.6.1",
4
- "description": "CLI for deploying Overscore dashboards",
3
+ "version": "0.8.0",
4
+ "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"
7
7
  },
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "skills",
11
+ "templates"
10
12
  ],
11
13
  "scripts": {
12
14
  "build": "tsc"
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: scaffold-overscore-artifact
3
+ description: Use when the user wants to create a new Overscore dashboard or analysis, or when they're inside one artifact type and want to switch to the other (e.g. "turn this analysis into a dashboard", "do a quick analysis on this dashboard's data", "I want to investigate why X happened", "I want to track X over time"). Follow this procedure to scaffold the new artifact in the correct location with the correct command and seed it with context from the current artifact.
4
+ ---
5
+
6
+ # Scaffolding Overscore Artifacts
7
+
8
+ Overscore has two artifact types. They are **parallel**, not a funnel. Knowing which one to use is the most important decision the user makes.
9
+
10
+ ## Dashboards vs. Analyses — the one-line difference
11
+
12
+ - **Dashboard** — a question you ask **forever**. Live React project, registered queries, scheduled cache refresh, deployed to a subdomain. Use when the user wants to *track* something over time.
13
+ - **Analysis** — a question you ask **once**. Static HTML report with numbers baked in, no React, no refreshes. Use when the user wants to *investigate* something specific and share the answer.
14
+
15
+ | If the user says... | Use |
16
+ |---|---|
17
+ | "I want to track X over time" | Dashboard |
18
+ | "I want to investigate why X happened" | Analysis |
19
+ | "I need this to refresh on a schedule" | Dashboard |
20
+ | "I want a one-page write-up to share in Slack" | Analysis |
21
+ | "I want a recurring view of our metrics" | Dashboard |
22
+ | "I want to do a one-off cohort analysis" | Analysis |
23
+
24
+ ## Analysis → Dashboard
25
+
26
+ If the user is inside an **analysis folder** (look for `.overscore/analysis.json`) and says something like "let's turn this into a real dashboard" or "make this update automatically":
27
+
28
+ **Do NOT** add React code, `package.json`, or any Vite scaffolding to the analysis folder. Analyses are static and that would corrupt the artifact.
29
+
30
+ Instead:
31
+
32
+ 1. Tell the user: "Analyses are frozen snapshots — to make this a tracked dashboard, we need to scaffold a new project. The analysis stays as the original investigation."
33
+ 2. Read `.overscore/analysis.json` to find the parent project slug.
34
+ 3. Tell them to run, from the parent directory:
35
+ ```bash
36
+ cd .. && npx create-overscore
37
+ ```
38
+ And use the same project slug (from `.overscore/analysis.json`) when prompted.
39
+ 4. Once the new dashboard folder exists, prepare a seed message for the new dashboard's first Claude session that includes:
40
+ - The analysis's question (from `analysis-log.md`)
41
+ - The tables involved (from `analysis-log.md` → "Tables involved" section)
42
+ - The key queries that mattered (from `analysis-log.md` → "Queries run")
43
+ - The conclusions reached
44
+ 5. Tell the user to paste this seed into Claude when they `cd` into the new dashboard.
45
+
46
+ The analysis remains intact as the original investigation. Don't delete it.
47
+
48
+ ## Dashboard → Analysis
49
+
50
+ If the user is inside a **dashboard folder** (look for `package.json` with `@overscore/client` dependency, or `.env` with `VITE_OVERSCORE_DASHBOARD_SLUG`) and says something like "I just want to do a quick one-off investigation" or "a write-up that won't be part of this dashboard":
51
+
52
+ **Do NOT** add throwaway components, ad-hoc routes, or unused queries to the dashboard. That pollutes a production artifact.
53
+
54
+ Instead:
55
+
56
+ 1. Tell the user: "That sounds like an Analysis — a one-off investigation that won't pollute this dashboard."
57
+ 2. Tell them to run, from the parent directory:
58
+ ```bash
59
+ cd .. && npx @overscore/cli analysis new "<title of the investigation>"
60
+ ```
61
+ 3. The new analysis folder will be a sibling of the dashboard, in the same project. The analysis's CLAUDE.md takes over from there.
62
+
63
+ ## Cold start (neither folder exists)
64
+
65
+ If the user is in a fresh terminal with no Overscore artifact present and says something like "I want to investigate X" or "I want to build a dashboard for Y":
66
+
67
+ 1. Match their intent against the table above.
68
+ 2. If it's an **analysis**: have them run `npx @overscore/cli analysis new "<title>"` (they'll be prompted for project + API key on first use).
69
+ 3. If it's a **dashboard**: have them run `npx create-overscore` and follow the prompts.
70
+ 4. Both commands scaffold a folder with a CLAUDE.md primer that takes over immediately.
71
+
72
+ ## Never confuse the two
73
+
74
+ An analysis folder has: `.overscore/analysis.json`, `analysis-log.md`, `report.html`, no `node_modules`.
75
+
76
+ A dashboard folder has: `package.json`, `node_modules`, `src/`, `vite.config.ts`, `.env` with `VITE_OVERSCORE_DASHBOARD_SLUG`.
77
+
78
+ If you see one shape and the user asks for behavior that belongs to the other shape, **always** route them to the correct command rather than wedging the wrong shape into the current folder.
@@ -0,0 +1,51 @@
1
+ # Overscore Analysis: {{TITLE}}
2
+
3
+ This is an **Overscore Analysis** — a one-off, frozen investigation that produces a single self-contained `report.html` you can share in Slack and reopen later to refresh.
4
+
5
+ Analyses are NOT dashboards. No React, no `package.json`, no `useQuery`, no scheduled refreshes. The numbers are baked into `report.html` at publish time.
6
+
7
+ ## First steps — interview before you query
8
+
9
+ **Before running any SQL, check `analysis-log.md`.** If it has no queries logged yet (this is a fresh analysis), invoke the **`start-analysis`** skill to interview the user first. Do NOT query data blindly — the user almost always knows things about the data that will save you 80% of the exploration work.
10
+
11
+ The interview takes 2 minutes and gives you:
12
+ - The real question (often different from the title)
13
+ - Which BigQuery tables matter (so you don't probe them all)
14
+ - What success looks like (a number? a chart? a go/no-go?)
15
+ - 1–3 hypotheses to test
16
+ - Time window and filters
17
+
18
+ After the interview, draft an initial entry in `analysis-log.md` and propose a query plan for the user to approve.
19
+
20
+ ## Always maintain analysis-log.md
21
+
22
+ `analysis-log.md` is the **recoverable session state** for this analysis. Future sessions (including yourself, weeks from now, after the user runs `analysis pull`) read it to know what's been done. **Append to it as you work**, not at the end.
23
+
24
+ Every meaningful exploration step is a new entry: the question being investigated, the SQL you ran (paste it verbatim), what you observed, the conclusion you drew, and any open questions for the next pass.
25
+
26
+ ## Where data context lives
27
+
28
+ `.claude/rules/company-context.md` was pulled from this analysis's parent project at scaffold time. It contains the project's table definitions, business logic, and data quirks shared across all dashboards and analyses in this project. **Read it first** before exploring blindly.
29
+
30
+ ## Publishing
31
+
32
+ When the analysis is ready, **tell the user to run** (do not run it yourself without asking):
33
+
34
+ ```bash
35
+ npx @overscore/cli analysis publish
36
+ ```
37
+
38
+ This uploads `report.html` and the entire bundle (this CLAUDE.md, `analysis-log.md`, the `.claude/` tree, etc.) to Overscore. The analysis becomes available at `https://<project>.overscore.dev/analysis/{{SLUG}}`. Re-publishing overwrites the previous version. **Always preview `report.html` in a browser and get the user's approval before publishing.**
39
+
40
+ ## When the user wants to turn this into a dashboard
41
+
42
+ Analyses are static. If the user asks to "make this a real dashboard" or "turn this into something that updates," **do NOT add React code to this folder** — it would corrupt the artifact. Use the global `scaffold-overscore-artifact` skill to route them to a fresh dashboard scaffold that lives as a sibling of this analysis.
43
+
44
+ ## See also
45
+
46
+ The following files in `.claude/rules/` load automatically when you touch matching files — you don't need to read them now:
47
+
48
+ - **`sql-style.md`** — BigQuery dialect and SQL formatting (loads when editing SQL or `analysis-log.md`)
49
+ - **`report-html.md`** — Self-contained HTML rules and dark-theme spec (loads when editing `report.html`)
50
+ - **`analysis-methodology.md`** — Investigation loop, when to ask vs query (loads when editing `analysis-log.md`)
51
+ - **`company-context.md`** — Project-level data context (loaded every session)
@@ -0,0 +1,63 @@
1
+ ---
2
+ paths:
3
+ - "analysis-log.md"
4
+ ---
5
+
6
+ # Analysis methodology
7
+
8
+ These rules apply when you're updating `analysis-log.md` — i.e., when you're actively investigating. They exist to keep you from drifting into "let me just run more queries" mode and to make sure every query you run actually moves the analysis forward.
9
+
10
+ ## The investigation loop
11
+
12
+ Every analysis is a loop, not a straight line:
13
+
14
+ 1. **Hypothesis** — write down what you think you'll find before running the query. One sentence.
15
+ 2. **Query** — write the SQL, log it in `analysis-log.md` with the hypothesis.
16
+ 3. **Observe** — read the result. Note the actual outcome, not just "the query ran."
17
+ 4. **Revise** — was the hypothesis right? If yes, you've moved one step. If no, what does the surprise tell you? Update your mental model.
18
+ 5. **Repeat** — pick the next hypothesis from what you just learned, not from a fixed list.
19
+
20
+ If you're running queries without a hypothesis attached, you're probing, not investigating. Stop and ask the user what they actually want to know.
21
+
22
+ ## Ask before you query
23
+
24
+ **Always ask the user, don't guess, when:**
25
+
26
+ - The question is ambiguous ("what's our churn?" — over what time window? by cohort or overall? for which product?)
27
+ - A time window isn't stated (last 7 days? last quarter? all time?)
28
+ - The data shape suggests multiple interpretations (does "active user" mean "logged in" or "performed an action"?)
29
+ - A filter could go multiple ways (include or exclude trial users? include or exclude test accounts?)
30
+ - You're about to run a query that will scan a lot of data (>100GB)
31
+
32
+ The 30-second cost of asking is always cheaper than the 5-minute cost of running the wrong query and having to redo it.
33
+
34
+ ## One hypothesis per query batch
35
+
36
+ Don't run 8 exploratory queries to "see what's there." Pick one hypothesis, run the 1-3 queries that test it, log them, observe, and then pick the next hypothesis based on what you learned. Batching by hypothesis keeps `analysis-log.md` readable and keeps you from accumulating noise.
37
+
38
+ ## Escalation: summary → detail
39
+
40
+ Always start with summary queries before detail queries:
41
+
42
+ 1. **Sizing pass:** `SELECT COUNT(*), MIN(date), MAX(date) FROM table` — how big is it, what time range does it cover?
43
+ 2. **Distribution pass:** `SELECT category, COUNT(*) FROM table GROUP BY 1 ORDER BY 2 DESC LIMIT 50` — what are the top values?
44
+ 3. **Trend pass:** `SELECT DATE_TRUNC(date, WEEK), COUNT(*) FROM table GROUP BY 1` — what's the shape over time?
45
+ 4. **Detail pass:** `SELECT * FROM table WHERE specific_thing LIMIT 100` — only after you know what to look for.
46
+
47
+ Jumping straight to detail queries before you understand the shape is the most common way to waste an investigation.
48
+
49
+ ## When to stop
50
+
51
+ You're done investigating when:
52
+
53
+ - You've answered the user's original question with enough confidence to act on it
54
+ - You've documented the answer in `analysis-log.md` and you can write `report.html` from the log alone
55
+ - Further queries are returning increasingly marginal information
56
+
57
+ You're NOT done when:
58
+
59
+ - You've found "interesting things" but haven't answered the original question
60
+ - Your conclusion is "more analysis needed" — that means you stopped too early; pick a hypothesis and keep going
61
+ - The user is going to ask "but what about X?" and you don't have an answer
62
+
63
+ When you think you're done, re-read the original question (it's at the top of `analysis-log.md`) and ask yourself: "If I sent this report to the user right now, would they have what they need?" If no, keep going. If yes, write `report.html`.
@@ -0,0 +1,83 @@
1
+ ---
2
+ paths:
3
+ - "report.html"
4
+ ---
5
+
6
+ # report.html — self-contained HTML report
7
+
8
+ `report.html` is the deliverable. It's what gets shared in Slack, opened by stakeholders, and reopened weeks later. It must work standalone with **no network requests at view time**.
9
+
10
+ ## Hard rules
11
+
12
+ - **Inline all CSS.** No `<link rel="stylesheet">`, no Tailwind CDN, no Google Fonts. Use a single `<style>` block in the `<head>`.
13
+ - **Bake the data into the HTML.** No `fetch()`, no `XMLHttpRequest`, no API calls. The numbers must be visible to anyone who opens the file with no logins, no tokens, no broken external dependencies.
14
+ - **Charts are inline.** Inline SVG is the safest option. Embedded base64 PNGs are OK. A self-contained chart library (Recharts/D3 from CDN) is acceptable **only if the data is already baked into the page** and the script tag is inline-loaded — but inline SVG is preferred because it survives indefinitely.
15
+ - **No `useQuery`, no `@overscore/client`, no React.** This is a static HTML file, not a React app.
16
+
17
+ ## Dark theme — Overscore brand
18
+
19
+ Match the Overscore visual style:
20
+
21
+ - **Background:** `#0a0a0f` (near-black)
22
+ - **Cards:** `rgba(255,255,255,0.03)` background with `1px solid rgba(255,255,255,0.06)` border, generous padding, `border-radius: 16px`
23
+ - **Text:** `#fff` for headlines and numbers, `rgba(255,255,255,0.6)` for body, `rgba(255,255,255,0.4)` for secondary
24
+ - **Accents:** violet `#8b5cf6`, blue `#3b82f6`, emerald `#10b981`, amber `#f59e0b`
25
+ - **Font:** `-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`
26
+ - **Spacing:** generous — `padding: 4rem 2rem` on the body, `max-width: 900px`, `margin: 0 auto` to center
27
+
28
+ ## Structure
29
+
30
+ A good analysis report has these sections in order:
31
+
32
+ 1. **Title** — the question being answered, not just the topic
33
+ 2. **Executive summary** — 2-3 sentences with the headline finding
34
+ 3. **KPI cards** — 3-5 of the most important numbers, each with context (% change, comparison, etc.)
35
+ 4. **Primary chart** — the main visualization that tells the story
36
+ 5. **Supporting charts** — 1-3 secondary visuals that drill into the why
37
+ 6. **Detail tables** — only if the reader needs precise numbers
38
+ 7. **Conclusions** — what the data says, in plain language
39
+ 8. **Methodology footer** — date generated, time window analyzed, any caveats
40
+
41
+ ## Skeleton
42
+
43
+ ```html
44
+ <!doctype html>
45
+ <html lang="en">
46
+ <head>
47
+ <meta charset="utf-8" />
48
+ <title>The headline finding goes here</title>
49
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
50
+ <style>
51
+ body {
52
+ background: #0a0a0f;
53
+ color: #fff;
54
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
55
+ max-width: 900px;
56
+ margin: 0 auto;
57
+ padding: 4rem 2rem;
58
+ line-height: 1.6;
59
+ }
60
+ .card {
61
+ background: rgba(255,255,255,0.03);
62
+ border: 1px solid rgba(255,255,255,0.06);
63
+ border-radius: 16px;
64
+ padding: 1.5rem;
65
+ margin: 1.5rem 0;
66
+ }
67
+ .kpi { font-size: 2.5rem; font-weight: 600; }
68
+ .label { color: rgba(255,255,255,0.4); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; }
69
+ h1, h2, h3 { font-weight: 600; }
70
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; }
71
+ .footer { margin-top: 4rem; color: rgba(255,255,255,0.3); font-size: 0.75rem; }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <h1>Headline finding here</h1>
76
+ <p style="color: rgba(255,255,255,0.6);">Executive summary in 2-3 sentences.</p>
77
+ <!-- KPI cards, charts, conclusions, etc. -->
78
+ <div class="footer">Generated YYYY-MM-DD · {{TITLE}}</div>
79
+ </body>
80
+ </html>
81
+ ```
82
+
83
+ **Always preview `report.html` in a browser and get the user's approval before publishing.**
@@ -0,0 +1,43 @@
1
+ ---
2
+ paths:
3
+ - "analysis-log.md"
4
+ - "*.sql"
5
+ - "**/*.sql"
6
+ ---
7
+
8
+ # SQL style — BigQuery
9
+
10
+ These rules apply when you're drafting SQL queries to paste into `analysis-log.md` or to test against the warehouse. Every query you run should also be logged verbatim in `analysis-log.md`.
11
+
12
+ ## Dialect — BigQuery Standard SQL
13
+
14
+ - Backtick fully-qualified table refs: `` `project.dataset.table` ``. Always use the project name when querying — it's explicit and unambiguous.
15
+ - Cast safely: `SAFE_CAST(col AS INT64)`, `SAFE_CAST(col AS DATE)`. A bad cast in vanilla SQL kills the query; `SAFE_CAST` returns `NULL`.
16
+ - Parse dates with `PARSE_DATE("%Y-%m-%d", col)` and timestamps with `PARSE_TIMESTAMP("%Y-%m-%d %H:%M:%S", col)`. Don't trust automatic coercion.
17
+ - For window functions, prefer `QUALIFY` over a subquery wrapper: `SELECT ... QUALIFY ROW_NUMBER() OVER (...) = 1`.
18
+ - Use `COUNTIF(condition)` instead of `SUM(CASE WHEN condition THEN 1 ELSE 0 END)`.
19
+ - Date arithmetic: `DATE_ADD(date, INTERVAL 7 DAY)`, `DATE_DIFF(d1, d2, DAY)`, `DATE_TRUNC(date, MONTH)`.
20
+
21
+ ## Formatting
22
+
23
+ - Uppercase keywords: `SELECT`, `FROM`, `WHERE`, `GROUP BY`, `ORDER BY`, `WITH`, `JOIN`, `ON`, `QUALIFY`.
24
+ - One clause per line. Don't cram `SELECT a, b FROM t WHERE x = 1` onto one line if it has more than ~3 columns.
25
+ - Trailing commas in column lists are fine in BigQuery and make diffs cleaner.
26
+ - Use CTEs (`WITH name AS (...)`) for any query with more than one logical step. Easier to read, easier to debug, easier to log.
27
+ - Two-space indentation inside parens and CTEs.
28
+ - Inline comments with `--` for non-obvious filters: `-- exclude test users`.
29
+
30
+ ## Safety on exploratory queries
31
+
32
+ - **Always include `LIMIT`** on initial exploratory queries unless you're aggregating. `SELECT * FROM big_table LIMIT 100` lets you see the shape without burning a query slot.
33
+ - For aggregations, use `GROUP BY ... ORDER BY count DESC LIMIT 50` to find the top contributors before you go wide.
34
+ - When you're scanning a table for the first time, run `SELECT COUNT(*), MIN(date_col), MAX(date_col) FROM ...` to learn the size and time range before doing anything else.
35
+
36
+ ## Logging discipline
37
+
38
+ **Every query you run goes into `analysis-log.md`** with:
39
+ 1. A one-line description of what it was investigating
40
+ 2. The exact SQL (paste it inside a fenced code block — don't paraphrase)
41
+ 3. A one-line summary of what you observed in the result
42
+
43
+ If you ran a query and didn't log it, you wasted the query — future-you can't reproduce or learn from it.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: start-analysis
3
+ description: Use at the start of any new Overscore analysis BEFORE querying data. Interviews the user about the question, known data sources, success criteria, and hypotheses, then writes an initial analysis-log.md entry and proposes a query plan for approval. Trigger when analysis-log.md has no queries logged yet.
4
+ ---
5
+
6
+ # Starting an analysis the right way
7
+
8
+ Most analyses fail because Claude jumps straight into querying every BigQuery table to "see what's there." That wastes 80% of the time on exploration the user could have skipped with a 2-minute conversation.
9
+
10
+ This skill exists to make sure that conversation happens before any SQL runs.
11
+
12
+ ## When to invoke this
13
+
14
+ - The user has just scaffolded a fresh analysis (`analysis-log.md` only has the seed entry, no queries logged)
15
+ - The user is asking you to "look at" or "investigate" something for the first time on this analysis
16
+ - You're about to write your first query and you're not sure what tables matter
17
+
18
+ Do NOT invoke this on a re-pull of an existing analysis with prior queries logged — that's a continuation, not a fresh start, and the prior log already has the context.
19
+
20
+ ## The interview — 6 questions, 2 minutes
21
+
22
+ Read `interview-template.md` for the literal prompts. The interview covers:
23
+
24
+ 1. **Restate the question** — confirm you understand what the user actually wants to know. The analysis title is often a shorthand; the real question is usually more specific.
25
+ 2. **Known tables** — ask what BigQuery tables the user already knows are relevant. Most users know at least 1-3 tables that matter. This saves you from probing every dataset.
26
+ 3. **Success criteria** — ask what "done" looks like. A single number? A chart? A go/no-go decision? Knowing this shapes everything downstream.
27
+ 4. **Hypotheses** — ask the user for 1-3 things they suspect might be true. Even rough hypotheses give you a query plan to test against.
28
+ 5. **Time window and filters** — ask what time range, what cohort, what to include/exclude. These are the most common sources of wasted queries when wrong.
29
+ 6. **Anything else weird** — ask "is there anything I should know about this data?" Users usually surface 1-2 known quirks ("dates before 2024 are unreliable," "we have a test_user flag we exclude").
30
+
31
+ Ask these one or two at a time conversationally — don't dump all 6 at once like a form. The user will respond more openly to a real conversation.
32
+
33
+ ## After the interview
34
+
35
+ 1. **Read `.claude/rules/company-context.md`** to cross-reference what the user said against what's already documented about the project's data.
36
+ 2. **Draft an initial entry in `analysis-log.md`** with:
37
+ - The clarified question (not the original title)
38
+ - The tables the user named, with what each contains
39
+ - The success criteria (literally — quote the user)
40
+ - The hypotheses (numbered)
41
+ - The time window and any filters
42
+ - Any quirks the user mentioned
43
+ 3. **Propose a prioritized query plan** — 3-5 queries that test the highest-priority hypothesis first. For each query, write the SQL, what hypothesis it tests, and what result would confirm or refute it. Do NOT run any of them yet.
44
+ 4. **Get approval.** Show the user the plan and ask them to confirm or revise before you run anything.
45
+
46
+ ## What this skill does NOT do
47
+
48
+ - It does not run any SQL. Querying happens after the user approves the plan.
49
+ - It does not write `report.html`. That happens at the end of the investigation, not the beginning.
50
+ - It does not skip the interview because the analysis title looks "obvious." Even obvious-sounding analyses benefit from the 2-minute conversation.
51
+
52
+ ## If the user resists the interview
53
+
54
+ Some users will say "just look at the data, I'll tell you if it's wrong." That's a trap — you'll spend 30 minutes probing tables and still need the interview at the end. Push back once: "I can save us a lot of time if you tell me what tables matter and what success looks like — it's a 2-minute conversation." If they still refuse, do a single sizing pass on the parent project's known tables (from `company-context.md`), then come back and ask narrower questions based on what you found.
@@ -0,0 +1,31 @@
1
+ # Interview prompts
2
+
3
+ These are the literal questions to ask the user during the start-analysis interview. Ask them one or two at a time, conversationally — don't dump them all at once. Wait for an answer before moving to the next.
4
+
5
+ ## 1. Restate the question
6
+
7
+ > Before I touch any data — let me make sure I understand what you actually want to know. The analysis title is "{{TITLE}}" — is the real question more specific than that? For example, are you looking at a particular time window, a particular cohort, or trying to make a specific decision?
8
+
9
+ ## 2. Known tables
10
+
11
+ > Which BigQuery tables do you already know are relevant here? Even if you only know one or two, that's a huge head start — I won't have to probe everything.
12
+
13
+ ## 3. Success criteria
14
+
15
+ > What does "done" look like for this analysis? Are we trying to land on a single number, build a chart, or make a go/no-go decision? Knowing the shape of the answer helps me know when to stop digging.
16
+
17
+ ## 4. Hypotheses
18
+
19
+ > What do you suspect might be true here? Even rough guesses help — I'd rather test 2-3 specific hypotheses than wander around looking for patterns. What's your gut telling you?
20
+
21
+ ## 5. Time window and filters
22
+
23
+ > What time window should we be looking at? And are there any filters I should apply by default — like excluding test users, internal accounts, or specific products?
24
+
25
+ ## 6. Anything weird about this data
26
+
27
+ > Last one: is there anything about this data I should know upfront? Quirks, known issues, things that look suspicious but are actually fine, conventions you use? Those are the things that bite if I find out about them three queries in instead of right now.
28
+
29
+ ---
30
+
31
+ After all 6 answers, summarize back what you heard and ask the user to confirm. Then write the initial entry in `analysis-log.md` and propose the query plan.
@@ -0,0 +1,17 @@
1
+ # {{TITLE}}
2
+
3
+ ## {{DATE}} — Initial exploration
4
+
5
+ **Question:** {{TITLE}}
6
+
7
+ ### Tables involved
8
+ - _(populate as you explore — see `.claude/rules/company-context.md` for what's available)_
9
+
10
+ ### Queries run
11
+ - _(append every meaningful query here, with one-line description + SQL)_
12
+
13
+ ### Conclusions
14
+ - _(empty so far)_
15
+
16
+ ### Open questions
17
+ - _(empty so far)_
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>{{TITLE}}</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ body { background: #0a0a0f; color: #fff; font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem 2rem; max-width: 800px; margin: 0 auto; }
9
+ h1 { font-weight: 600; }
10
+ p { color: rgba(255,255,255,0.6); }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <h1>{{TITLE}}</h1>
15
+ <p>This analysis is empty. Have Claude do the work and regenerate this file.</p>
16
+ </body>
17
+ </html>