@overscore/cli 0.7.0 → 0.8.1
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 +107 -151
- package/package.json +3 -2
- 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
|
@@ -711,97 +711,87 @@ async function analysisApiRequest(cfg, method, urlPath, body) {
|
|
|
711
711
|
}
|
|
712
712
|
return data;
|
|
713
713
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
3. Once they're in the new dashboard folder, hand off a seed message that includes: this analysis's question, the tables explored (from \`analysis-log.md\`), the key queries that mattered, and the conclusions reached. The new dashboard's Claude session can use this as a starting point.
|
|
796
|
-
|
|
797
|
-
## Do NOT
|
|
798
|
-
|
|
799
|
-
- Add \`package.json\`, \`node_modules\`, or any React/Vite scaffolding to this folder.
|
|
800
|
-
- Use \`useQuery()\` or any Overscore client hooks — they don't exist here.
|
|
801
|
-
- Call BigQuery from inside \`report.html\` at view time. Bake the numbers in.
|
|
802
|
-
- Publish without showing the user \`report.html\` in a browser first and getting their approval.
|
|
803
|
-
- Forget to update \`analysis-log.md\` as you work.
|
|
804
|
-
`;
|
|
714
|
+
/**
|
|
715
|
+
* Fetch hub-managed rule files (company-context, sql-style) for a project
|
|
716
|
+
* and write them into the target analysis folder, overwriting whatever's
|
|
717
|
+
* there. Best-effort and non-fatal — if the hub is unreachable, the existing
|
|
718
|
+
* files (template defaults or bundle contents) stay in place.
|
|
719
|
+
*
|
|
720
|
+
* Called from both `analysis new` (after the template walker) and `analysis
|
|
721
|
+
* pull` (after the bundle extraction) so hub overrides always reflect the
|
|
722
|
+
* latest state regardless of how the analysis was created.
|
|
723
|
+
*/
|
|
724
|
+
async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
|
|
725
|
+
const authHeader = { Authorization: `Bearer ${apiKey}` };
|
|
726
|
+
const rulesDir = path.join(targetDir, ".claude", "rules");
|
|
727
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
728
|
+
// Company context — null means "no company context set yet"; leave any
|
|
729
|
+
// existing file alone in that case.
|
|
730
|
+
try {
|
|
731
|
+
const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, { headers: authHeader });
|
|
732
|
+
if (ctxRes.ok) {
|
|
733
|
+
const ctxData = (await ctxRes.json());
|
|
734
|
+
if (ctxData.context_md) {
|
|
735
|
+
fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// Non-fatal
|
|
741
|
+
}
|
|
742
|
+
// SQL style override — null means "use the CLI default"; leave the
|
|
743
|
+
// template/bundle file alone in that case.
|
|
744
|
+
try {
|
|
745
|
+
const sqlRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/sql-style`, { headers: authHeader });
|
|
746
|
+
if (sqlRes.ok) {
|
|
747
|
+
const sqlData = (await sqlRes.json());
|
|
748
|
+
if (sqlData.sql_style_md) {
|
|
749
|
+
fs.writeFileSync(path.join(rulesDir, "sql-style.md"), sqlData.sql_style_md);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
// Non-fatal
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Recursively copies a template directory to a target directory, substituting
|
|
759
|
+
* `{{KEY}}` placeholders in text files. Mirrors directory structure.
|
|
760
|
+
*
|
|
761
|
+
* Substitutions are applied to .md, .html, .json, .yml, .yaml files. Other
|
|
762
|
+
* file types are copied byte-for-byte.
|
|
763
|
+
*/
|
|
764
|
+
function scaffoldFromTemplate(templateDir, targetDir, substitutions) {
|
|
765
|
+
const TEXT_EXTENSIONS = new Set([".md", ".html", ".json", ".yml", ".yaml"]);
|
|
766
|
+
function substitute(content) {
|
|
767
|
+
let out = content;
|
|
768
|
+
for (const [key, value] of Object.entries(substitutions)) {
|
|
769
|
+
out = out.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
function walk(srcDir, destDir) {
|
|
774
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
775
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
776
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
777
|
+
const destPath = path.join(destDir, entry.name);
|
|
778
|
+
if (entry.isDirectory()) {
|
|
779
|
+
walk(srcPath, destPath);
|
|
780
|
+
}
|
|
781
|
+
else if (entry.isFile()) {
|
|
782
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
783
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
784
|
+
const content = fs.readFileSync(srcPath, "utf8");
|
|
785
|
+
fs.writeFileSync(destPath, substitute(content));
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
fs.copyFileSync(srcPath, destPath);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
walk(templateDir, targetDir);
|
|
794
|
+
}
|
|
805
795
|
async function analysisNew(title) {
|
|
806
796
|
if (!title) {
|
|
807
797
|
console.error('\n Usage: npx @overscore/cli analysis new "<title>"\n');
|
|
@@ -819,10 +809,18 @@ async function analysisNew(title) {
|
|
|
819
809
|
console.error(`\n Error: Directory "${folderName}" already exists.\n`);
|
|
820
810
|
process.exit(1);
|
|
821
811
|
}
|
|
812
|
+
// Scaffold from the template tree shipped with the package.
|
|
813
|
+
// __dirname after build is dist/, so the templates dir is one level up.
|
|
814
|
+
const distDir = path.dirname(new URL(import.meta.url).pathname);
|
|
815
|
+
const pkgRoot = path.resolve(distDir, "..");
|
|
816
|
+
const templateDir = path.join(pkgRoot, "templates", "analysis");
|
|
817
|
+
if (!fs.existsSync(templateDir)) {
|
|
818
|
+
console.error(`\n Error: Bundled analysis template not found at ${templateDir}\n`);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
822
821
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
823
|
-
fs.mkdirSync(path.join(targetDir, ".claude", "rules"), { recursive: true });
|
|
824
822
|
fs.mkdirSync(path.join(targetDir, ".overscore"), { recursive: true });
|
|
825
|
-
// Write analysis.json metadata
|
|
823
|
+
// Write analysis.json metadata (not part of the template — has dynamic id)
|
|
826
824
|
fs.writeFileSync(path.join(targetDir, ".overscore", "analysis.json"), JSON.stringify({
|
|
827
825
|
id: data.id,
|
|
828
826
|
slug: data.slug,
|
|
@@ -830,65 +828,18 @@ async function analysisNew(title) {
|
|
|
830
828
|
project_slug: data.project_slug,
|
|
831
829
|
created_at: new Date().toISOString(),
|
|
832
830
|
}, null, 2));
|
|
833
|
-
//
|
|
834
|
-
const claudeMd = ANALYSIS_CLAUDE_MD
|
|
835
|
-
.replace(/\{\{TITLE\}\}/g, data.title)
|
|
836
|
-
.replace(/\{\{SLUG\}\}/g, data.slug);
|
|
837
|
-
fs.writeFileSync(path.join(targetDir, ".claude", "CLAUDE.md"), claudeMd);
|
|
838
|
-
// Empty analysis log seeded with the question
|
|
831
|
+
// Walk the template tree, substituting placeholders
|
|
839
832
|
const today = new Date().toISOString().slice(0, 10);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
- _(append every meaningful query here, with one-line description + SQL)_
|
|
851
|
-
|
|
852
|
-
### Conclusions
|
|
853
|
-
- _(empty so far)_
|
|
854
|
-
|
|
855
|
-
### Open questions
|
|
856
|
-
- _(empty so far)_
|
|
857
|
-
`;
|
|
858
|
-
fs.writeFileSync(path.join(targetDir, "analysis-log.md"), analysisLog);
|
|
859
|
-
// Empty report.html stub
|
|
860
|
-
fs.writeFileSync(path.join(targetDir, "report.html"), `<!doctype html>
|
|
861
|
-
<html lang="en">
|
|
862
|
-
<head>
|
|
863
|
-
<meta charset="utf-8" />
|
|
864
|
-
<title>${data.title}</title>
|
|
865
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
866
|
-
<style>
|
|
867
|
-
body { background: #0a0a0f; color: #fff; font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem 2rem; max-width: 800px; margin: 0 auto; }
|
|
868
|
-
h1 { font-weight: 600; }
|
|
869
|
-
p { color: rgba(255,255,255,0.6); }
|
|
870
|
-
</style>
|
|
871
|
-
</head>
|
|
872
|
-
<body>
|
|
873
|
-
<h1>${data.title}</h1>
|
|
874
|
-
<p>This analysis is empty. Have Claude do the work and regenerate this file.</p>
|
|
875
|
-
</body>
|
|
876
|
-
</html>
|
|
877
|
-
`);
|
|
878
|
-
// Pull project's company-context.md (best-effort, non-fatal)
|
|
879
|
-
try {
|
|
880
|
-
const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
|
|
881
|
-
const ctxRes = await fetch(`${baseUrl}/api/projects/${data.project_slug}/context`, { headers: { Authorization: `Bearer ${cfg.apiKey}` } });
|
|
882
|
-
if (ctxRes.ok) {
|
|
883
|
-
const ctxData = (await ctxRes.json());
|
|
884
|
-
if (ctxData.context_md) {
|
|
885
|
-
fs.writeFileSync(path.join(targetDir, ".claude", "rules", "company-context.md"), ctxData.context_md);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
catch {
|
|
890
|
-
// Non-fatal
|
|
891
|
-
}
|
|
833
|
+
scaffoldFromTemplate(templateDir, targetDir, {
|
|
834
|
+
TITLE: data.title,
|
|
835
|
+
SLUG: data.slug,
|
|
836
|
+
PROJECT_SLUG: data.project_slug,
|
|
837
|
+
DATE: today,
|
|
838
|
+
});
|
|
839
|
+
// Pull hub-managed rules (company context + sql style override). Best-effort
|
|
840
|
+
// and non-fatal — template defaults stay in place if the hub is unreachable.
|
|
841
|
+
const baseUrl = cfg.apiUrl.replace(/\/api$/, "");
|
|
842
|
+
await syncHubManagedRules(targetDir, baseUrl, data.project_slug, cfg.apiKey);
|
|
892
843
|
console.log(` Analysis created: ${data.title}
|
|
893
844
|
Slug: ${data.slug}
|
|
894
845
|
Project: ${data.project_slug}
|
|
@@ -1029,6 +980,11 @@ async function analysisPull(slug) {
|
|
|
1029
980
|
// Non-fatal
|
|
1030
981
|
}
|
|
1031
982
|
}
|
|
983
|
+
// Refresh hub-managed rule files after restoring from the bundle. The
|
|
984
|
+
// bundle contains whatever was set at publish time; this overwrites them
|
|
985
|
+
// with the latest hub state so company context and SQL style stay in
|
|
986
|
+
// sync across pulls. Best-effort and non-fatal.
|
|
987
|
+
await syncHubManagedRules(targetDir, baseUrl, projectSlug, cfg.apiKey);
|
|
1032
988
|
console.log(`
|
|
1033
989
|
Pulled "${analysisTitle}"
|
|
1034
990
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@overscore/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
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
9
|
"dist",
|
|
10
|
-
"skills"
|
|
10
|
+
"skills",
|
|
11
|
+
"templates"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsc"
|
|
@@ -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>
|