@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 +424 -1
- package/package.json +5 -3
- package/skills/scaffold-overscore-artifact/SKILL.md +78 -0
- package/templates/analysis/.claude/CLAUDE.md +51 -0
- package/templates/analysis/.claude/rules/analysis-methodology.md +63 -0
- package/templates/analysis/.claude/rules/report-html.md +83 -0
- package/templates/analysis/.claude/rules/sql-style.md +43 -0
- package/templates/analysis/.claude/skills/start-analysis/SKILL.md +54 -0
- package/templates/analysis/.claude/skills/start-analysis/interview-template.md +31 -0
- package/templates/analysis/analysis-log.md +17 -0
- package/templates/analysis/report.html +17 -0
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>
|
|
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.
|
|
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>
|