@overscore/cli 0.10.0 → 0.10.2

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
@@ -72,7 +72,7 @@ function loadEnv() {
72
72
  }
73
73
  process.exit(1);
74
74
  }
75
- async function apiRequest(method, urlPath, body) {
75
+ async function apiRequest(method, urlPath, body, command) {
76
76
  const { apiUrl, apiKey } = loadEnv();
77
77
  const baseUrl = apiUrl.replace(/\/api$/, "");
78
78
  const url = `${baseUrl}${urlPath}`;
@@ -82,6 +82,9 @@ async function apiRequest(method, urlPath, body) {
82
82
  if (body !== undefined) {
83
83
  headers["Content-Type"] = "application/json";
84
84
  }
85
+ if (command) {
86
+ headers["X-Overscore-Command"] = command;
87
+ }
85
88
  const res = await fetch(url, {
86
89
  method,
87
90
  headers,
@@ -261,7 +264,10 @@ async function deploy() {
261
264
  // Sync context files to Hub
262
265
  const { projectSlug } = loadEnv();
263
266
  const baseUrl = apiUrl.replace(/\/api$/, "");
264
- // Sync company context (with conflict detection)
267
+ // Upload knowledge base (project-level, shared across all artifacts)
268
+ await uploadKnowledgeBase(process.cwd(), baseUrl, projectSlug, apiKey);
269
+ // Sync company context (with conflict detection) — legacy path for
270
+ // projects that haven't migrated to the knowledge base yet.
265
271
  const companyCtxPath = path.resolve(process.cwd(), ".claude/rules/company-context.md");
266
272
  if (fs.existsSync(companyCtxPath)) {
267
273
  const content = fs.readFileSync(companyCtxPath, "utf-8");
@@ -304,9 +310,11 @@ async function deploy() {
304
310
  }
305
311
  }
306
312
  // 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");
313
+ const dashCtxPath = path.resolve(process.cwd(), ".claude/rules/this-dashboard.md");
314
+ const legacyDashCtxPath = path.resolve(process.cwd(), ".claude/rules/dashboard-context.md");
315
+ const activeDashCtxPath = fs.existsSync(dashCtxPath) ? dashCtxPath : fs.existsSync(legacyDashCtxPath) ? legacyDashCtxPath : null;
316
+ if (activeDashCtxPath) {
317
+ const content = fs.readFileSync(activeDashCtxPath, "utf-8");
310
318
  try {
311
319
  await fetch(`${baseUrl}/api/dashboards/${dashboardSlug}/context`, {
312
320
  method: "PUT",
@@ -327,23 +335,45 @@ async function deploy() {
327
335
  }
328
336
  async function queryList() {
329
337
  const { dashboardSlug } = loadEnv();
330
- const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`));
338
+ const data = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-list"));
331
339
  console.log(`\n Dashboard: ${data.dashboard}\n`);
340
+ // Registered queries
332
341
  if (data.queries.length === 0) {
333
342
  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;
343
+ console.log(" Add a SQL query: npx @overscore/cli query add <name> \"SELECT ...\"\n");
337
344
  }
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}`);
345
+ else {
346
+ console.log(" Registered Queries:\n");
347
+ for (const q of data.queries) {
348
+ const rowInfo = q.row_count !== null ? ` (~${q.row_count.toLocaleString()} rows)` : "";
349
+ const sourceTag = q.source === "dataset" ? " [dataset]" : "";
350
+ console.log(` ${q.query_name}${rowInfo}${sourceTag}`);
351
+ console.log(` useQuery("${q.query_name}")`);
352
+ if (q.sample_row) {
353
+ const cols = Object.keys(q.sample_row).join(", ");
354
+ console.log(` columns: ${cols}`);
355
+ }
356
+ else if (q.columns && q.columns.length > 0) {
357
+ const cols = q.columns.map((c) => `${c.name} (${c.type})`).join(", ");
358
+ console.log(` columns: ${cols}`);
359
+ }
360
+ console.log();
361
+ }
362
+ }
363
+ // Project datasets (always shown)
364
+ if (data.project_datasets && data.project_datasets.length > 0) {
365
+ console.log(" Project Datasets:\n");
366
+ for (const ds of data.project_datasets) {
367
+ const rowInfo = ds.row_count !== null ? ` (~${ds.row_count.toLocaleString()} rows)` : "";
368
+ const sampleTag = ds.is_sample ? " [sample]" : "";
369
+ console.log(` ${ds.name}${rowInfo}${sampleTag}`);
370
+ if (ds.columns && ds.columns.length > 0) {
371
+ const cols = ds.columns.map((c) => `${c.name} (${c.type})`).join(", ");
372
+ console.log(` columns: ${cols}`);
373
+ }
374
+ console.log(` To use: npx @overscore/cli query add <query-name> --dataset ${ds.name}`);
375
+ console.log();
345
376
  }
346
- console.log();
347
377
  }
348
378
  }
349
379
  async function queryShow(name) {
@@ -365,16 +395,28 @@ async function queryShow(name) {
365
395
  }
366
396
  console.log(`\n ${query.query_name}`);
367
397
  if (query.row_count !== null) {
368
- console.log(` ~${query.row_count.toLocaleString()} rows\n`);
398
+ console.log(` ~${query.row_count.toLocaleString()} rows`);
369
399
  }
370
- else {
371
- console.log();
400
+ if (query.source === "dataset" && query.dataset_name) {
401
+ console.log(` source: uploaded dataset (${query.dataset_name})`);
372
402
  }
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`);
403
+ console.log();
404
+ if (query.source === "dataset") {
405
+ if (query.columns && query.columns.length > 0) {
406
+ console.log(` Columns:`);
407
+ for (const col of query.columns) {
408
+ console.log(` ${col.name} (${col.type})`);
409
+ }
410
+ console.log();
411
+ }
412
+ }
413
+ else if (query.sql) {
414
+ console.log(` SQL:`);
415
+ console.log(` ${query.sql}\n`);
416
+ if (query.sample_row) {
417
+ console.log(` Sample row:`);
418
+ console.log(` ${JSON.stringify(query.sample_row, null, 2).split("\n").join("\n ")}\n`);
419
+ }
378
420
  }
379
421
  }
380
422
  function resolveSql(sqlOrNext) {
@@ -391,10 +433,15 @@ function resolveSql(sqlOrNext) {
391
433
  return sqlOrNext;
392
434
  }
393
435
  async function queryAdd(name, sql) {
436
+ // Check for --dataset flag
437
+ const datasetFlagIndex = process.argv.indexOf("--dataset");
438
+ const datasetName = datasetFlagIndex !== -1 ? process.argv[datasetFlagIndex + 1] : undefined;
394
439
  const resolvedSql = resolveSql(sql);
395
- if (!name || !resolvedSql) {
440
+ if (!name || (!resolvedSql && !datasetName)) {
396
441
  console.error('\n Usage: npx @overscore/cli query add <name> "<sql>"\n');
397
- console.error(" Or with a file:");
442
+ console.error(" Or link an uploaded dataset:");
443
+ console.error(" npx @overscore/cli query add <name> --dataset <dataset-name>\n");
444
+ console.error(" Or with a SQL file:");
398
445
  console.error(" npx @overscore/cli query add <name> --file query.sql\n");
399
446
  process.exit(1);
400
447
  }
@@ -403,7 +450,26 @@ async function queryAdd(name, sql) {
403
450
  process.exit(1);
404
451
  }
405
452
  const { dashboardSlug } = loadEnv();
406
- const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, { query_name: name, sql_text: resolvedSql }));
453
+ let body;
454
+ if (datasetName) {
455
+ // Look up the dataset UUID from project_datasets in the queries endpoint
456
+ const listData = (await apiRequest("GET", `/api/dashboards/${dashboardSlug}/queries`, undefined, "query-add"));
457
+ const dataset = (listData.project_datasets ?? []).find((ds) => ds.name === datasetName);
458
+ if (!dataset) {
459
+ console.error(`\n Error: Dataset "${datasetName}" not found in this project.\n`);
460
+ console.error(" Available datasets:");
461
+ for (const ds of listData.project_datasets ?? []) {
462
+ console.error(` - ${ds.name}`);
463
+ }
464
+ console.error();
465
+ process.exit(1);
466
+ }
467
+ body = { query_name: name, dataset_id: dataset.id };
468
+ }
469
+ else {
470
+ body = { query_name: name, sql_text: resolvedSql };
471
+ }
472
+ const data = (await apiRequest("POST", `/api/dashboards/${dashboardSlug}/queries`, body, "query-add"));
407
473
  console.log(`\n Query "${data.query_name}" created.\n`);
408
474
  console.log(` Use it in your dashboard:`);
409
475
  console.log(` const { data } = useQuery("${data.query_name}");\n`);
@@ -435,24 +501,34 @@ async function queryRemove(name) {
435
501
  await apiRequest("DELETE", `/api/dashboards/${dashboardSlug}/queries/${encodeURIComponent(name)}`);
436
502
  console.log(`\n Query "${name}" deleted.\n`);
437
503
  }
438
- function saveQueryResult(sql, data) {
504
+ function saveQueryResult(sql, data, sourceFile) {
439
505
  try {
440
506
  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
507
  const dataDir = path.resolve(process.cwd(), "data");
508
+ // Check for ANY existing file with this hash — handles both old and new
509
+ // filename formats so the same query isn't duplicated.
510
+ if (fs.existsSync(dataDir)) {
511
+ const existing = fs.readdirSync(dataDir).find((f) => f.includes(hash) && f.endsWith(".json"));
512
+ if (existing) {
513
+ return { path: `data/${existing}`, cached: true };
514
+ }
515
+ }
516
+ // Build filename: human-readable prefix when from a file, hash prefix otherwise
517
+ let filename;
518
+ if (sourceFile) {
519
+ const baseName = path.basename(sourceFile, path.extname(sourceFile));
520
+ filename = `${baseName}__${hash}.json`;
521
+ }
522
+ else {
523
+ const snippet = sql
524
+ .toLowerCase()
525
+ .replace(/[^a-z0-9_]+/g, "_")
526
+ .replace(/^_|_$/g, "")
527
+ .slice(0, 40);
528
+ filename = `${hash}_${snippet}.json`;
529
+ }
448
530
  const fullPath = path.join(dataDir, filename);
449
531
  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
532
  if (!fs.existsSync(dataDir)) {
457
533
  fs.mkdirSync(dataDir, { recursive: true });
458
534
  }
@@ -501,7 +577,9 @@ async function queryTest(sql) {
501
577
  process.exit(1);
502
578
  }
503
579
  const data = (await apiRequest("POST", `/api/projects/${projectSlug}/test-query`, { sql: resolvedSql }));
504
- const saved = saveQueryResult(resolvedSql, data);
580
+ const fileIndex = process.argv.indexOf("--file");
581
+ const sourceFile = fileIndex !== -1 ? process.argv[fileIndex + 1] : undefined;
582
+ const saved = saveQueryResult(resolvedSql, data, sourceFile);
505
583
  const savedLine = saved
506
584
  ? saved.cached
507
585
  ? ` Cached (already saved earlier): ${saved.path}`
@@ -657,7 +735,7 @@ VITE_OVERSCORE_API_URL=https://overscore.dev/api
657
735
  if (dashCtxData.context_md) {
658
736
  const rulesDir = path.join(targetDir, ".claude", "rules");
659
737
  fs.mkdirSync(rulesDir, { recursive: true });
660
- fs.writeFileSync(path.join(rulesDir, "dashboard-context.md"), dashCtxData.context_md);
738
+ fs.writeFileSync(path.join(rulesDir, "this-dashboard.md"), dashCtxData.context_md);
661
739
  console.log(" Updated dashboard context from Hub.");
662
740
  }
663
741
  }
@@ -889,19 +967,23 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
889
967
  const authHeader = { Authorization: `Bearer ${apiKey}` };
890
968
  const rulesDir = path.join(targetDir, ".claude", "rules");
891
969
  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);
970
+ // Try knowledge base first if the project has knowledge files, they
971
+ // replace the legacy company-context.md.
972
+ const hasKnowledge = await syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey);
973
+ // Company context only fetch if no knowledge files exist (legacy path).
974
+ if (!hasKnowledge) {
975
+ try {
976
+ const ctxRes = await fetch(`${baseUrl}/api/projects/${projectSlug}/context`, { headers: authHeader });
977
+ if (ctxRes.ok) {
978
+ const ctxData = (await ctxRes.json());
979
+ if (ctxData.context_md) {
980
+ fs.writeFileSync(path.join(rulesDir, "company-context.md"), ctxData.context_md);
981
+ }
900
982
  }
901
983
  }
902
- }
903
- catch {
904
- // Non-fatal
984
+ catch {
985
+ // Non-fatal
986
+ }
905
987
  }
906
988
  // SQL style override — null means "use the CLI default"; leave the
907
989
  // template/bundle file alone in that case.
@@ -918,6 +1000,93 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
918
1000
  // Non-fatal
919
1001
  }
920
1002
  }
1003
+ /**
1004
+ * Sync the project knowledge base from the Hub into .claude/knowledge/.
1005
+ * Returns true if knowledge files were found (caller can skip legacy
1006
+ * company-context.md in that case).
1007
+ */
1008
+ async function syncKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
1009
+ const authHeader = { Authorization: `Bearer ${apiKey}` };
1010
+ const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
1011
+ try {
1012
+ const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, { headers: authHeader });
1013
+ if (!res.ok)
1014
+ return false;
1015
+ const data = (await res.json());
1016
+ if (!data.files || data.files.length === 0)
1017
+ return false;
1018
+ fs.mkdirSync(knowledgeDir, { recursive: true });
1019
+ // Write each knowledge file
1020
+ for (const file of data.files) {
1021
+ fs.writeFileSync(path.join(knowledgeDir, file.path), file.content);
1022
+ }
1023
+ // Generate INDEX.md
1024
+ const indexLines = [
1025
+ "# Project Knowledge Base",
1026
+ "",
1027
+ "Read the relevant file when working on a topic. Do NOT read all files every session.",
1028
+ "",
1029
+ ];
1030
+ for (const file of data.files) {
1031
+ indexLines.push(`- **${file.title}**: \`.claude/knowledge/${file.path}\``);
1032
+ }
1033
+ indexLines.push("");
1034
+ fs.writeFileSync(path.join(knowledgeDir, "INDEX.md"), indexLines.join("\n"));
1035
+ // Store hashes for conflict detection on upload
1036
+ const hashMap = {};
1037
+ for (const file of data.files) {
1038
+ hashMap[file.path] = file.content_hash;
1039
+ }
1040
+ fs.writeFileSync(path.join(knowledgeDir, ".knowledge-hashes"), JSON.stringify(hashMap, null, 2));
1041
+ console.log(` Knowledge base synced (${data.files.length} files).`);
1042
+ return true;
1043
+ }
1044
+ catch {
1045
+ return false;
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Upload local knowledge files back to the Hub.
1050
+ * Called from deploy/publish so knowledge learned during a session persists.
1051
+ */
1052
+ async function uploadKnowledgeBase(targetDir, baseUrl, projectSlug, apiKey) {
1053
+ const knowledgeDir = path.join(targetDir, ".claude", "knowledge");
1054
+ if (!fs.existsSync(knowledgeDir))
1055
+ return;
1056
+ const files = [];
1057
+ for (const entry of fs.readdirSync(knowledgeDir)) {
1058
+ // Skip INDEX.md (auto-generated) and dotfiles (.knowledge-hashes)
1059
+ if (entry === "INDEX.md" ||
1060
+ entry.startsWith(".") ||
1061
+ !entry.endsWith(".md"))
1062
+ continue;
1063
+ const content = fs.readFileSync(path.join(knowledgeDir, entry), "utf-8");
1064
+ // Extract title from first H1 line, fallback to filename
1065
+ const titleMatch = content.match(/^#\s+(.+)$/m);
1066
+ const title = titleMatch
1067
+ ? titleMatch[1]
1068
+ : entry.replace(/\.md$/, "").replace(/[-_]/g, " ");
1069
+ files.push({ path: entry, title, content });
1070
+ }
1071
+ if (files.length === 0)
1072
+ return;
1073
+ try {
1074
+ const res = await fetch(`${baseUrl}/api/projects/${projectSlug}/knowledge`, {
1075
+ method: "PUT",
1076
+ headers: {
1077
+ Authorization: `Bearer ${apiKey}`,
1078
+ "Content-Type": "application/json",
1079
+ },
1080
+ body: JSON.stringify({ files }),
1081
+ });
1082
+ if (res.ok) {
1083
+ console.log(` Knowledge base synced (${files.length} files).`);
1084
+ }
1085
+ }
1086
+ catch {
1087
+ // Non-fatal
1088
+ }
1089
+ }
921
1090
  /**
922
1091
  * Fetch the latest platform-owned Claude Code rules from the Hub and write
923
1092
  * them into the project's .claude/ directory. This keeps rules like
@@ -925,8 +1094,8 @@ async function syncHubManagedRules(targetDir, baseUrl, projectSlug, apiKey) {
925
1094
  * projects that were scaffolded months ago.
926
1095
  *
927
1096
  * 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.
1097
+ * User-owned files (company-context.md, this-dashboard.md, knowledge/)
1098
+ * are NOT touched — those sync via separate mechanisms.
930
1099
  */
931
1100
  async function syncPlatformRules(targetDir, baseUrl, apiKey) {
932
1101
  try {
@@ -1273,6 +1442,8 @@ async function analysisPublish() {
1273
1442
  console.log(`\n Published successfully!\n`);
1274
1443
  printBundleSummary(files, process.cwd());
1275
1444
  console.log(`\n URL: ${data.url}\n`);
1445
+ // Upload knowledge base so insights from this analysis persist
1446
+ await uploadKnowledgeBase(process.cwd(), baseUrl, meta.project_slug, cfg.apiKey);
1276
1447
  }
1277
1448
  /**
1278
1449
  * Ensure analysis.json has a valid id by fetching from the Hub.
@@ -1844,7 +2015,8 @@ else if (command === "query") {
1844
2015
  Commands:
1845
2016
  list List all queries
1846
2017
  show <name> Show a query's SQL and sample data
1847
- add <name> "<sql>" Add a new query
2018
+ add <name> "<sql>" Add a SQL query
2019
+ add <name> --dataset <name> Link an uploaded dataset as a query
1848
2020
  update <name> "<sql>" Update a query's SQL
1849
2021
  remove <name> Delete a query
1850
2022
  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.2",
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