@overscore/cli 0.10.0 → 0.10.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 CHANGED
@@ -261,7 +261,10 @@ async function deploy() {
261
261
  // Sync context files to Hub
262
262
  const { projectSlug } = loadEnv();
263
263
  const baseUrl = apiUrl.replace(/\/api$/, "");
264
- // Sync company context (with conflict detection)
264
+ // Upload knowledge base (project-level, shared across all artifacts)
265
+ await uploadKnowledgeBase(process.cwd(), baseUrl, projectSlug, apiKey);
266
+ // Sync company context (with conflict detection) — legacy path for
267
+ // projects that haven't migrated to the knowledge base yet.
265
268
  const companyCtxPath = path.resolve(process.cwd(), ".claude/rules/company-context.md");
266
269
  if (fs.existsSync(companyCtxPath)) {
267
270
  const content = fs.readFileSync(companyCtxPath, "utf-8");
@@ -304,9 +307,11 @@ async function deploy() {
304
307
  }
305
308
  }
306
309
  // Sync dashboard context (no conflict detection needed — per-dashboard)
307
- const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard-context.md");
308
- if (fs.existsSync(dashCtxPath)) {
309
- const content = fs.readFileSync(dashCtxPath, "utf-8");
310
+ const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/this-dashboard.md");
311
+ const legacyDashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard-context.md");
312
+ const activeDashCtxPath = fs.existsSync(dashCtxPath) ? dashCtxPath : fs.existsSync(legacyDashCtxPath) ? legacyDashCtxPath : null;
313
+ if (activeDashCtxPath) {
314
+ const content = fs.readFileSync(activeDashCtxPath, "utf-8");
310
315
  try {
311
316
  await fetch(`${baseUrl}/api/dashboards/${dashboardSlug}/context`, {
312
317
  method: "PUT",
@@ -329,21 +334,43 @@ async function queryList() {
329
334
  const { dashboardSlug } = loadEnv();
330
335
  const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
331
336
  console.log(`\n Dashboard: ${data.dashboard}\n`);
337
+ // Registered queries
332
338
  if (data.queries.length === 0) {
333
339
  console.log(" No queries registered yet.\n");
334
- console.log(" Add one with:");
335
- console.log(' npx @overscore/cli query add <name> "SELECT ..."\n');
336
- return;
340
+ console.log(" Add a SQL query: npx @overscore/cli query add <name> \"SELECT ...\"\n");
337
341
  }
338
- for (const q of data.queries) {
339
- const rowInfo = q.row_count !== null ? ` (~${q.row_count.toLocaleString()} rows)` : "";
340
- console.log(` ${q.query_name}${rowInfo}`);
341
- console.log(` useQuery("${q.query_name}")`);
342
- if (q.sample_row) {
343
- const cols = Object.keys(q.sample_row).join(", ");
344
- console.log(` columns: ${cols}`);
342
+ else {
343
+ console.log(" Registered Queries:\n");
344
+ for (const q of data.queries) {
345
+ const rowInfo = q.row_count !== null ? ` (~${q.row_count.toLocaleString()} rows)` : "";
346
+ const sourceTag = q.source === "dataset" ? " [dataset]" : "";
347
+ console.log(` ${q.query_name}${rowInfo}${sourceTag}`);
348
+ console.log(` useQuery("${q.query_name}")`);
349
+ if (q.sample_row) {
350
+ const cols = Object.keys(q.sample_row).join(", ");
351
+ console.log(` columns: ${cols}`);
352
+ }
353
+ else if (q.columns && q.columns.length > 0) {
354
+ const cols = q.columns.map((c) => `${c.name} (${c.type})`).join(", ");
355
+ console.log(` columns: ${cols}`);
356
+ }
357
+ console.log();
358
+ }
359
+ }
360
+ // Project datasets (always shown)
361
+ if (data.project_datasets && data.project_datasets.length > 0) {
362
+ console.log(" Project Datasets:\n");
363
+ for (const ds of data.project_datasets) {
364
+ const rowInfo = ds.row_count !== null ? ` (~${ds.row_count.toLocaleString()} rows)` : "";
365
+ const sampleTag = ds.is_sample ? " [sample]" : "";
366
+ console.log(` ${ds.name}${rowInfo}${sampleTag}`);
367
+ if (ds.columns && ds.columns.length > 0) {
368
+ const cols = ds.columns.map((c) => `${c.name} (${c.type})`).join(", ");
369
+ console.log(` columns: ${cols}`);
370
+ }
371
+ console.log(` To use: npx @overscore/cli query add <query-name> --dataset ${ds.name}`);
372
+ console.log();
345
373
  }
346
- console.log();
347
374
  }
348
375
  }
349
376
  async function queryShow(name) {
@@ -365,16 +392,28 @@ async function queryShow(name) {
365
392
  }
366
393
  console.log(`\n ${query.query_name}`);
367
394
  if (query.row_count !== null) {
368
- console.log(` ~${query.row_count.toLocaleString()} rows\n`);
395
+ console.log(` ~${query.row_count.toLocaleString()} rows`);
369
396
  }
370
- else {
371
- console.log();
397
+ if (query.source === "dataset" && query.dataset_name) {
398
+ console.log(` source: uploaded dataset (${query.dataset_name})`);
372
399
  }
373
- console.log(` SQL:`);
374
- console.log(` ${query.sql}\n`);
375
- if (query.sample_row) {
376
- console.log(` Sample row:`);
377
- console.log(` ${JSON.stringify(query.sample_row, null, 2).split("\n").join("\n ")}\n`);
400
+ console.log();
401
+ if (query.source === "dataset") {
402
+ if (query.columns && query.columns.length > 0) {
403
+ console.log(` Columns:`);
404
+ for (const col of query.columns) {
405
+ console.log(` ${col.name} (${col.type})`);
406
+ }
407
+ console.log();
408
+ }
409
+ }
410
+ else if (query.sql) {
411
+ console.log(` SQL:`);
412
+ console.log(` ${query.sql}\n`);
413
+ if (query.sample_row) {
414
+ console.log(` Sample row:`);
415
+ console.log(` ${JSON.stringify(query.sample_row, null, 2).split("\n").join("\n ")}\n`);
416
+ }
378
417
  }
379
418
  }
380
419
  function resolveSql(sqlOrNext) {
@@ -391,10 +430,15 @@ function resolveSql(sqlOrNext) {
391
430
  return sqlOrNext;
392
431
  }
393
432
  async function queryAdd(name, sql) {
433
+ // Check for --dataset flag
434
+ const datasetFlagIndex = process.argv.indexOf("--dataset");
435
+ const datasetName = datasetFlagIndex !== -1 ? process.argv[datasetFlagIndex + 1] : undefined;
394
436
  const resolvedSql = resolveSql(sql);
395
- if (!name || !resolvedSql) {
437
+ if (!name || (!resolvedSql && !datasetName)) {
396
438
  console.error('\n Usage: npx @overscore/cli query add <name> "<sql>"\n');
397
- console.error(" Or with a file:");
439
+ console.error(" Or link an uploaded dataset:");
440
+ console.error(" npx @overscore/cli query add <name> --dataset <dataset-name>\n");
441
+ console.error(" Or with a SQL file:");
398
442
  console.error(" npx @overscore/cli query add <name> --file query.sql\n");
399
443
  process.exit(1);
400
444
  }
@@ -403,7 +447,26 @@ async function queryAdd(name, sql) {
403
447
  process.exit(1);
404
448
  }
405
449
  const { dashboardSlug } = loadEnv();
406
- const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, { query_name: name, sql_text: resolvedSql }));
450
+ let body;
451
+ if (datasetName) {
452
+ // Look up the dataset UUID from project_datasets in the queries endpoint
453
+ const listData = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
454
+ const dataset = (listData.project_datasets ?? []).find((ds) => ds.name === datasetName);
455
+ if (!dataset) {
456
+ console.error(`\n Error: Dataset "${datasetName}" not found in this project.\n`);
457
+ console.error(" Available datasets:");
458
+ for (const ds of listData.project_datasets ?? []) {
459
+ console.error(` - ${ds.name}`);
460
+ }
461
+ console.error();
462
+ process.exit(1);
463
+ }
464
+ body = { query_name: name, dataset_id: dataset.id };
465
+ }
466
+ else {
467
+ body = { query_name: name, sql_text: resolvedSql };
468
+ }
469
+ const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, body));
407
470
  console.log(`\n Query "${data.query_name}" created.\n`);
408
471
  console.log(` Use it in your dashboard:`);
409
472
  console.log(` const { data } = useQuery("${data.query_name}");\n`);
@@ -435,24 +498,34 @@ async function queryRemove(name) {
435
498
  await apiRequest("DELETE", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`);
436
499
  console.log(`\n Query "${name}" deleted.\n`);
437
500
  }
438
- function saveQueryResult(sql, data) {
501
+ function saveQueryResult(sql, data, sourceFile) {
439
502
  try {
440
503
  const hash = createHash("sha256").update(sql).digest("hex").slice(0, 8);
441
- const snippet = sql
442
- .toLowerCase()
443
- .replace(/[^a-z0-9_]+/g, "_")
444
- .replace(/^_|_$/g, "")
445
- .slice(0, 40);
446
- const filename = `${hash}_${snippet}.json`;
447
504
  const dataDir = path.resolve(process.cwd(), "data");
505
+ // Check for ANY existing file with this hash — handles both old and new
506
+ // filename formats so the same query isn't duplicated.
507
+ if (fs.existsSync(dataDir)) {
508
+ const existing = fs.readdirSync(dataDir).find((f) => f.includes(hash) && f.endsWith(".json"));
509
+ if (existing) {
510
+ return { path: `data/${existing}`, cached: true };
511
+ }
512
+ }
513
+ // Build filename: human-readable prefix when from a file, hash prefix otherwise
514
+ let filename;
515
+ if (sourceFile) {
516
+ const baseName = path.basename(sourceFile, path.extname(sourceFile));
517
+ filename = `${baseName}__${hash}.json`;
518
+ }
519
+ else {
520
+ const snippet = sql
521
+ .toLowerCase()
522
+ .replace(/[^a-z0-9_]+/g, "_")
523
+ .replace(/^_|_$/g, "")
524
+ .slice(0, 40);
525
+ filename = `${hash}_${snippet}.json`;
526
+ }
448
527
  const fullPath = path.join(dataDir, filename);
449
528
  const relPath = `data/${filename}`;
450
- // Hash is content-derived from the SQL — if a file with this hash already
451
- // exists, the exact same query was run before. Skip the rewrite so the
452
- // data/ folder doesn't accumulate duplicates of identical queries.
453
- if (fs.existsSync(fullPath)) {
454
- return { path: relPath, cached: true };
455
- }
456
529
  if (!fs.existsSync(dataDir)) {
457
530
  fs.mkdirSync(dataDir, { recursive: true });
458
531
  }
@@ -501,7 +574,9 @@ async function queryTest(sql) {
501
574
  process.exit(1);
502
575
  }
503
576
  const data = (await apiRequest("POST", `/api/projects/${projectSlug}/test-query`, { sql: resolvedSql }));
504
- const saved = saveQueryResult(resolvedSql, data);
577
+ const fileIndex = process.argv.indexOf("--file");
578
+ const sourceFile = fileIndex !== -1 ? process.argv[fileIndex + 1] : undefined;
579
+ const saved = saveQueryResult(resolvedSql, data, sourceFile);
505
580
  const savedLine = saved
506
581
  ? saved.cached
507
582
  ? ` Cached (already saved earlier): ${saved.path}`
@@ -657,7 +732,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
657
732
  if (dashCtxData.context_md) {
658
733
  const rulesDir = path.join(targetDir, ".claude", "rules");
659
734
  fs.mkdirSync(rulesDir, { recursive: true });
660
- fs.writeFileSync(path.join(rulesDir, "dashboard-context.md"), dashCtxData.context_md);
735
+ fs.writeFileSync(path.join(rulesDir, "this-dashboard.md"), dashCtxData.context_md);
661
736
  console.log(" Updated dashboard context from Hub.");
662
737
  }
663
738
  }
@@ -889,19 +964,23 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
889
964
  const authHeader = { Authorization: `Bearer ${apiKey}` };
890
965
  const rulesDir = path.join(targetDir, ".claude", "rules");
891
966
  fs.mkdirSync(rulesDir, { recursive: true });
892
- // Company context null means "no company context set yet"; leave any
893
- // existing file alone in that case.
894
- try {
895
- const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, { headers: authHeader });
896
- if (ctxRes.ok) {
897
- const ctxData = (await ctxRes.json());
898
- if (ctxData.context_md) {
899
- fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
967
+ // Try knowledge base first if the project has knowledge files, they
968
+ // replace the legacy company-context.md.
969
+ const hasKnowledge = await syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey);
970
+ // Company context only fetch if no knowledge files exist (legacy path).
971
+ if (!hasKnowledge) {
972
+ try {
973
+ const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, { headers: authHeader });
974
+ if (ctxRes.ok) {
975
+ const ctxData = (await ctxRes.json());
976
+ if (ctxData.context_md) {
977
+ fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
978
+ }
900
979
  }
901
980
  }
902
- }
903
- catch {
904
- // Non-fatal
981
+ catch {
982
+ // Non-fatal
983
+ }
905
984
  }
906
985
  // SQL style override — null means "use the CLI default"; leave the
907
986
  // template/bundle file alone in that case.
@@ -918,6 +997,93 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
918
997
  // Non-fatal
919
998
  }
920
999
  }
1000
+ /**
1001
+ * Sync the project knowledge base from the Hub into .claude/knowledge/.
1002
+ * Returns true if knowledge files were found (caller can skip legacy
1003
+ * company-context.md in that case).
1004
+ */
1005
+ async function syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
1006
+ const authHeader = { Authorization: `Bearer ${apiKey}` };
1007
+ const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
1008
+ try {
1009
+ const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, { headers: authHeader });
1010
+ if (!res.ok)
1011
+ return false;
1012
+ const data = (await res.json());
1013
+ if (!data.files || data.files.length === 0)
1014
+ return false;
1015
+ fs.mkdirSync(knowledgeDir, { recursive: true });
1016
+ // Write each knowledge file
1017
+ for (const file of data.files) {
1018
+ fs.writeFileSync(path.join(knowledgeDir, file.path), file.content);
1019
+ }
1020
+ // Generate INDEX.md
1021
+ const indexLines = [
1022
+ "# Project Knowledge Base",
1023
+ "",
1024
+ "Read the relevant file when working on a topic. Do NOT read all files every session.",
1025
+ "",
1026
+ ];
1027
+ for (const file of data.files) {
1028
+ indexLines.push(`- **${file.title}**: \`.claude/knowledge/${file.path}\``);
1029
+ }
1030
+ indexLines.push("");
1031
+ fs.writeFileSync(path.join(knowledgeDir, "INDEX.md"), indexLines.join("\n"));
1032
+ // Store hashes for conflict detection on upload
1033
+ const hashMap = {};
1034
+ for (const file of data.files) {
1035
+ hashMap[file.path] = file.content_hash;
1036
+ }
1037
+ fs.writeFileSync(path.join(knowledgeDir, ".knowledge-hashes"), JSON.stringify(hashMap, null, 2));
1038
+ console.log(` Knowledge base synced (${data.files.length} files).`);
1039
+ return true;
1040
+ }
1041
+ catch {
1042
+ return false;
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Upload local knowledge files back to the Hub.
1047
+ * Called from deploy/publish so knowledge learned during a session persists.
1048
+ */
1049
+ async function uploadKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
1050
+ const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
1051
+ if (!fs.existsSync(knowledgeDir))
1052
+ return;
1053
+ const files = [];
1054
+ for (const entry of fs.readdirSync(knowledgeDir)) {
1055
+ // Skip INDEX.md (auto-generated) and dotfiles (.knowledge-hashes)
1056
+ if (entry === "INDEX.md" ||
1057
+ entry.startsWith(".") ||
1058
+ !entry.endsWith(".md"))
1059
+ continue;
1060
+ const content = fs.readFileSync(path.join(knowledgeDir, entry), "utf-8");
1061
+ // Extract title from first H1 line, fallback to filename
1062
+ const titleMatch = content.match(/^#\s+(.+)$/m);
1063
+ const title = titleMatch
1064
+ ? titleMatch[1]
1065
+ : entry.replace(/\.md$/, "").replace(/[-_]/g, " ");
1066
+ files.push({ path: entry, title, content });
1067
+ }
1068
+ if (files.length === 0)
1069
+ return;
1070
+ try {
1071
+ const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, {
1072
+ method: "PUT",
1073
+ headers: {
1074
+ Authorization: `Bearer ${apiKey}`,
1075
+ "Content-Type": "application/json",
1076
+ },
1077
+ body: JSON.stringify({ files }),
1078
+ });
1079
+ if (res.ok) {
1080
+ console.log(` Knowledge base synced (${files.length} files).`);
1081
+ }
1082
+ }
1083
+ catch {
1084
+ // Non-fatal
1085
+ }
1086
+ }
921
1087
  /**
922
1088
  * Fetch the latest platform-owned Claude Code rules from the Hub and write
923
1089
  * them into the project's .claude/ directory. This keeps rules like
@@ -925,8 +1091,8 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
925
1091
  * projects that were scaffolded months ago.
926
1092
  *
927
1093
  * Called from `deploy` (before uploading) and `pull` (after extracting).
928
- * User-owned files (company-context.md, dashboard-context.md) are NOT
929
- * touched — those sync via separate mechanisms.
1094
+ * User-owned files (company-context.md, this-dashboard.md, knowledge/)
1095
+ * are NOT touched — those sync via separate mechanisms.
930
1096
  */
931
1097
  async function syncPlatformRules(targetDir, baseUrl, apiKey) {
932
1098
  try {
@@ -1273,6 +1439,8 @@ async function analysisPublish() {
1273
1439
  console.log(`\n Published successfully!\n`);
1274
1440
  printBundleSummary(files, process.cwd());
1275
1441
  console.log(`\n URL: ${data.url}\n`);
1442
+ // Upload knowledge base so insights from this analysis persist
1443
+ await uploadKnowledgeBase(process.cwd(), baseUrl, meta.project_slug, cfg.apiKey);
1276
1444
  }
1277
1445
  /**
1278
1446
  * Ensure analysis.json has a valid id by fetching from the Hub.
@@ -1844,7 +2012,8 @@ else if (command === "query") {
1844
2012
  Commands:
1845
2013
  list List all queries
1846
2014
  show <name> Show a query's SQL and sample data
1847
- add <name> "<sql>" Add a new query
2015
+ add <name> "<sql>" Add a SQL query
2016
+ add <name> --dataset <name> Link an uploaded dataset as a query
1848
2017
  update <name> "<sql>" Update a query's SQL
1849
2018
  remove <name> Delete a query
1850
2019
  run "<sql>" Run SQL against BigQuery
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: save-knowledge
3
+ description: Reviews data insights discovered during dashboard or analysis work and proposes additions to the project knowledge base. Use before deploying a dashboard or publishing an analysis, when the user says "save what you learned", or after discovering non-obvious data facts like timezone quirks, column semantics, dedup rules, or metric definitions.
4
+ ---
5
+
6
+ # Save Knowledge
7
+
8
+ Run this skill before deploying a dashboard or when explicitly asked.
9
+
10
+ ## When to invoke
11
+ - Before `npx @overscore/cli deploy`
12
+ - When the user says "save what you learned" or similar
13
+
14
+ ## Process
15
+
16
+ 1. Review the dashboard code and any exploratory queries you ran.
17
+
18
+ 2. Read `.claude/knowledge/INDEX.md` to see what's already documented.
19
+
20
+ 3. For each new insight, decide:
21
+ - **Project knowledge** (helps ALL future artifacts) → propose addition to `.claude/knowledge/`
22
+ - **Dashboard-specific** (design decision, query note) → append to `.claude/rules/this-dashboard.md`
23
+ - **Already known** → skip
24
+
25
+ 4. Present proposed additions to the user for approval before writing any files.
26
+
27
+ ## What qualifies as project knowledge vs dashboard-specific
28
+ - "dates are UTC-7 no DST" → project knowledge (helps all artifacts)
29
+ - "affiliate_id = 0 is unattributed" → project knowledge
30
+ - "Revenue chart excludes refunds per Finance definition" → dashboard-specific
31
+ - "Default filter is 90 days for CFO's current-quarter view" → dashboard-specific
@@ -1,6 +1,8 @@
1
1
  ---
2
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.
3
+ description: Routes users to the correct Overscore artifact type (Dashboard vs Analysis) and scaffolds it in the right location. Dashboards track metrics forever with live data; Analyses answer a question once with a static HTML report.
4
+ when_to_use: Use when the user wants to create a new dashboard or analysis, or when they're inside one artifact type and want to switch to the other. Trigger phrases include "build a dashboard", "track X over time", "create an analysis", "investigate why X", "turn this into a dashboard", "do a quick analysis", "I want to monitor", "I need a report on".
5
+ argument-hint: "[dashboard or analysis description]"
4
6
  ---
5
7
 
6
8
  # Scaffolding Overscore Artifacts
@@ -51,9 +51,20 @@ The CLI authenticates via `~/.overscore/config` (set up on first use). Credentia
51
51
 
52
52
  The `queries/` directory is for SQL files you want to keep around — the canonical version of each Q-numbered query in `analysis-log.md`, queries the user might want to re-run later, or anything you've iterated on enough to deserve a name. File names like `q3-cohort-halo.sql` cross-reference the Query Log. Exploratory one-offs don't need to live here; they're already saved as JSON in `data/`.
53
53
 
54
- ## Where data context lives
54
+ ## Project Knowledge Base
55
55
 
56
- `.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.
56
+ `.claude/knowledge/INDEX.md` lists all knowledge files for this project table definitions,
57
+ business logic, metric formulas, data quirks. **Read INDEX.md before starting any data work.**
58
+ Only read the specific files relevant to your current task.
59
+
60
+ When you discover something non-obvious (a timezone gotcha, a dedup rule, a column that means
61
+ something unexpected), **propose adding it to the knowledge base**:
62
+ - Create a new `.claude/knowledge/{topic}.md` file (one topic per file, 20-80 lines)
63
+ - Or append to an existing file if the topic matches
64
+ - Update INDEX.md when you add or remove a file
65
+
66
+ Knowledge files sync to the Hub on `analysis publish` and are shared with every artifact
67
+ in this project. Good knowledge notes help every future session.
57
68
 
58
69
  ## Previewing
59
70
 
@@ -96,4 +107,4 @@ The following files in `.claude/rules/` load automatically when you touch matchi
96
107
  - **`report-html.md`** — Self-contained HTML rules and dark-theme spec (loads when editing `report.html`)
97
108
  - **`report-components.md`** — Copy-paste HTML/SVG component snippets: KPI cards, charts, tables, heatmaps (loads when editing `report.html`)
98
109
  - **`analysis-methodology.md`** — Investigation loop, when to ask vs query (loads when editing `analysis-log.md`)
99
- - **`company-context.md`** — Project-level data context (loaded every session)
110
+ - **`knowledge/INDEX.md`** — Project knowledge base catalog (read before data work)
@@ -80,4 +80,11 @@ A good analysis report has these sections in order:
80
80
  </html>
81
81
  ```
82
82
 
83
+ **Mobile responsiveness.** Reports are often opened on phones from Slack links. The skeleton's `max-width: 900px` and viewport meta tag handle the basics, but also:
84
+ - Use `clamp()` or media queries for KPI font sizes (`font-size: clamp(1.5rem, 5vw, 2.5rem)`) so they don't overflow on 375px screens.
85
+ - KPI card grids should wrap: `display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr))`.
86
+ - Tables need `overflow-x: auto` on their container.
87
+ - Padding: `2rem` is fine on desktop but use `padding: 2rem 1rem` on mobile via `@media (max-width: 480px)`.
88
+ - Charts (SVG): set `width="100%" viewBox="..."` and let them scale, or use `max-width: 100%`.
89
+
83
90
  **Always preview `report.html` in a browser and get the user's approval before publishing.**
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: save-knowledge
3
+ description: Reviews data insights discovered during analysis work and proposes additions to the project knowledge base. Use before publishing an analysis, when the user says "save what you learned", or after discovering non-obvious data facts like timezone quirks, column semantics, dedup rules, or metric definitions.
4
+ ---
5
+
6
+ # Save Knowledge
7
+
8
+ Run this skill before publishing an analysis or when explicitly asked.
9
+
10
+ ## When to invoke
11
+ - Before `analysis publish` (review what you learned first)
12
+ - When the user says "save what you learned" or similar
13
+
14
+ ## Process
15
+
16
+ 1. Review `analysis-log.md` — look for non-obvious data facts you discovered:
17
+ - Column semantics that weren't obvious from the name
18
+ - Timezone, dedup, or join gotchas
19
+ - Metric definitions or business rules
20
+ - Data quality issues or known gaps
21
+
22
+ 2. Read `.claude/knowledge/INDEX.md` to see what's already documented.
23
+
24
+ 3. For each new insight, decide:
25
+ - **Project knowledge** (helps ALL future artifacts) → propose a new file or addition to `.claude/knowledge/`
26
+ - **Already known** (in an existing knowledge file) → skip
27
+ - **Analysis-specific** (only relevant to this investigation) → leave in analysis-log.md
28
+
29
+ 4. Present proposed additions to the user for approval before writing any files.
30
+
31
+ 5. After approval, write the files and update INDEX.md.
32
+
33
+ ## What qualifies as project knowledge
34
+ - "unique_page_views is already COUNT(DISTINCT domain_userid)" → YES
35
+ - "date_az is America/Phoenix UTC-7, no DST" → YES
36
+ - "affiliate_id = 0 is unattributed, usually exclude" → YES
37
+ - "Q1 churn spiked because of the pricing change" → NO (analysis conclusion, not data fact)
38
+ - "I used a 90-day window for this analysis" → NO (analysis-specific decision)
@@ -1,6 +1,11 @@
1
1
  ---
2
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.
3
+ description: Interviews the user before querying data at the start of a new Overscore analysis. Captures the real question, known tables, success criteria, hypotheses, time window, and data quirks, then writes an initial analysis-log.md entry and proposes a query plan for approval. Use when analysis-log.md has no queries logged yet, or when the user says "investigate", "look into", or "analyze" something for the first time.
4
+ allowed-tools:
5
+ - Read
6
+ - Write
7
+ - Edit
8
+ - Bash(npx @overscore/cli query run *)
4
9
  ---
5
10
 
6
11
  # Starting an analysis the right way
@@ -32,7 +37,7 @@ Ask these one or two at a time conversationally — don't dump all 6 at once lik
32
37
 
33
38
  ## After the interview
34
39
 
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.
40
+ 1. **Read `.claude/knowledge/INDEX.md`** and any relevant knowledge files to understand what previous sessions have already documented about this project's data. Cross-reference what the user said against what's already known.
36
41
  2. **Draft an initial entry in `analysis-log.md`** with:
37
42
  - The clarified question (not the original title)
38
43
  - The tables the user named, with what each contains
@@ -51,4 +56,4 @@ Ask these one or two at a time conversationally — don't dump all 6 at once lik
51
56
 
52
57
  ## If the user resists the interview
53
58
 
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.
59
+ 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 the knowledge base), then come back and ask narrower questions based on what you found.
@@ -9,3 +9,4 @@
9
9
  data/
10
10
  *.draft.html
11
11
  *.email.html
12
+ .knowledge-hashes