create-dss-project 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/bin/create-dss-project.js +4 -0
  2. package/lib/index.js +80 -0
  3. package/lib/project-types.js +74 -0
  4. package/lib/prompts.js +42 -0
  5. package/lib/scaffold.js +169 -0
  6. package/package.json +30 -0
  7. package/template/.github/workflows/dashboard-build.yml +27 -0
  8. package/template/.github/workflows/template-lint.yml +71 -0
  9. package/template/CHANGELOG.md +43 -0
  10. package/template/CLAUDE.md +145 -0
  11. package/template/LICENSE +21 -0
  12. package/template/README.md +201 -0
  13. package/template/STATUS.md +34 -0
  14. package/template/context/competitor-snapshot.md +27 -0
  15. package/template/context/market-snapshot.md +32 -0
  16. package/template/context/pipeline-state.md +36 -0
  17. package/template/context/project-state.md +45 -0
  18. package/template/dashboard/CLAUDE.md +36 -0
  19. package/template/dashboard/DEPLOY.md +60 -0
  20. package/template/dashboard/build-data.js +395 -0
  21. package/template/dashboard/competitors.html +143 -0
  22. package/template/dashboard/css/styles.css +143 -0
  23. package/template/dashboard/data/.gitkeep +0 -0
  24. package/template/dashboard/decisions.html +132 -0
  25. package/template/dashboard/index.html +152 -0
  26. package/template/dashboard/js/app.js +59 -0
  27. package/template/dashboard/js/overview.js +50 -0
  28. package/template/dashboard/js/sidebar.js +62 -0
  29. package/template/dashboard/js/tailwind-config.js +52 -0
  30. package/template/dashboard/package-lock.json +351 -0
  31. package/template/dashboard/package.json +17 -0
  32. package/template/dashboard/pipeline.html +149 -0
  33. package/template/dashboard/research.html +215 -0
  34. package/template/dashboard/robots.txt +2 -0
  35. package/template/dashboard/scoring.html +187 -0
  36. package/template/dashboard/timeline.html +165 -0
  37. package/template/dashboard/vercel.json +5 -0
  38. package/template/dashboard/watch.js +57 -0
  39. package/template/data/.gitkeep +0 -0
  40. package/template/discovery/calls/.gitkeep +0 -0
  41. package/template/discovery/outreach/.gitkeep +0 -0
  42. package/template/discovery/prep/.gitkeep +0 -0
  43. package/template/docs/decks/.gitkeep +0 -0
  44. package/template/docs/executive-summary.md +104 -0
  45. package/template/docs/getting-started.md +274 -0
  46. package/template/docs/memos/evidence-grading.md +27 -0
  47. package/template/docs/memos/housekeeping-reference.md +101 -0
  48. package/template/docs/memos/reference-context.md +30 -0
  49. package/template/docs/output/project-activity.md +8 -0
  50. package/template/docs/output/status-blurb.md +4 -0
  51. package/template/docs/output/work-log.md +8 -0
  52. package/template/docs/skill-authoring-guide.md +212 -0
  53. package/template/memory/MEMORY.md +84 -0
  54. package/template/memory/decisions.md +13 -0
  55. package/template/memory/discovery.md +48 -0
  56. package/template/memory/research.md +33 -0
  57. package/template/memory/scoring.md +34 -0
  58. package/template/project.config.example.json +31 -0
  59. package/template/research/competitors/.gitkeep +0 -0
  60. package/template/research/market/.gitkeep +0 -0
  61. package/template/research/technical/.gitkeep +0 -0
  62. package/template/scripts/.gitkeep +0 -0
  63. package/template/scripts/build-cli-template.sh +32 -0
  64. package/template/scripts/health-check.sh +152 -0
  65. package/template/scripts/reset-to-template.sh +115 -0
  66. package/template/scripts/validate-placeholders.sh +47 -0
  67. package/template/skills/compare-options/SKILL.md +97 -0
  68. package/template/skills/critical-reasoning/SKILL.md +107 -0
  69. package/template/skills/decision/SKILL.md +75 -0
  70. package/template/skills/enrich-entity/SKILL.md +107 -0
  71. package/template/skills/health-check/SKILL.md +144 -0
  72. package/template/skills/onboard/SKILL.md +434 -0
  73. package/template/skills/outreach-sequence/SKILL.md +79 -0
  74. package/template/skills/pipeline-update/SKILL.md +90 -0
  75. package/template/skills/process-call/SKILL.md +96 -0
  76. package/template/skills/rebuild-snapshots/SKILL.md +88 -0
  77. package/template/skills/session-end/SKILL.md +120 -0
  78. package/template/skills/session-start/SKILL.md +93 -0
  79. package/template/skills/synthesise/SKILL.md +108 -0
  80. package/template/skills/weekly-report/SKILL.md +79 -0
  81. package/template/templates/call-notes.md +67 -0
  82. package/template/templates/call-prep.md +65 -0
  83. package/template/templates/entity-teardown.md +58 -0
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script for {{PROJECT_NAME}} Dashboard
4
+ * Reads project markdown/CSV files (read-only) and outputs JSON to dashboard/data/
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { parse } = require('csv-parse/sync');
10
+ const { marked } = require('marked');
11
+
12
+ // Safety: hardcoded paths
13
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
14
+ const OUTPUT_DIR = path.join(__dirname, 'data');
15
+
16
+ // Ensure output dir exists and is inside dashboard/
17
+ if (!OUTPUT_DIR.startsWith(__dirname)) {
18
+ console.error('SAFETY: Output directory must be inside dashboard/');
19
+ process.exit(1);
20
+ }
21
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
22
+
23
+ // ─── Project Config ──────────────────────────────────────────────────────────
24
+
25
+ function loadProjectConfig() {
26
+ const configPath = path.join(PROJECT_ROOT, 'project.config.json');
27
+ try {
28
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
29
+ } catch (e) {
30
+ console.warn(' WARN: No project.config.json found — using defaults');
31
+ return {};
32
+ }
33
+ }
34
+
35
+ const PROJECT_CONFIG = loadProjectConfig();
36
+
37
+ // ─── Utilities ───────────────────────────────────────────────────────────────
38
+
39
+ function readFile(relativePath) {
40
+ const full = path.join(PROJECT_ROOT, relativePath);
41
+ try {
42
+ return fs.readFileSync(full, 'utf-8');
43
+ } catch (e) {
44
+ console.warn(` WARN: Could not read ${relativePath}`);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function parseMarkdownTables(md) {
50
+ const tables = [];
51
+ const lines = md.split('\n');
52
+ let i = 0;
53
+ while (i < lines.length) {
54
+ if (lines[i].trim().startsWith('|') && i + 1 < lines.length && lines[i + 1].trim().startsWith('|')) {
55
+ const tableLines = [];
56
+ while (i < lines.length && lines[i].trim().startsWith('|')) {
57
+ tableLines.push(lines[i]);
58
+ i++;
59
+ }
60
+ if (tableLines.length >= 3) {
61
+ const headers = tableLines[0].split('|').map(c => c.trim()).filter(c => c);
62
+ const rows = [];
63
+ for (let r = 2; r < tableLines.length; r++) {
64
+ const cells = tableLines[r].split('|').map(c => c.trim()).filter(c => c);
65
+ if (cells.length > 0 && !cells[0].match(/^[-:]+$/)) {
66
+ const row = {};
67
+ headers.forEach((h, idx) => { row[h] = cells[idx] || ''; });
68
+ rows.push(row);
69
+ }
70
+ }
71
+ if (rows.length > 0) tables.push({ headers, rows });
72
+ }
73
+ } else {
74
+ i++;
75
+ }
76
+ }
77
+ return tables;
78
+ }
79
+
80
+ function splitByHeadings(md, level = 2) {
81
+ const regex = new RegExp(`^${'#'.repeat(level)} (.+)$`, 'gm');
82
+ const sections = {};
83
+ let match;
84
+ const positions = [];
85
+ while ((match = regex.exec(md)) !== null) {
86
+ positions.push({ title: match[1].trim(), start: match.index + match[0].length });
87
+ }
88
+ for (let i = 0; i < positions.length; i++) {
89
+ const end = i + 1 < positions.length ? positions[i + 1].start - positions[i + 1].title.length - level - 2 : md.length;
90
+ const content = md.substring(positions[i].start, end).trim();
91
+ sections[positions[i].title] = content;
92
+ }
93
+ return sections;
94
+ }
95
+
96
+ // ─── Build Overview Data ─────────────────────────────────────────────────────
97
+
98
+ function buildOverview() {
99
+ console.log('Building overview data...');
100
+ const overview = {
101
+ projectName: PROJECT_CONFIG.projectName || '',
102
+ entityType: PROJECT_CONFIG.entityType || '',
103
+ statusBlurb: '',
104
+ killConditions: [],
105
+ buildTime: new Date().toISOString(),
106
+ };
107
+
108
+ // Status blurb
109
+ const blurb = readFile('docs/output/status-blurb.md');
110
+ if (blurb) {
111
+ overview.statusBlurb = blurb
112
+ .replace(/<!--[\s\S]*?-->/g, '')
113
+ .trim();
114
+ }
115
+
116
+ // Kill conditions from executive summary
117
+ const execSummary = readFile('docs/executive-summary.md');
118
+ if (execSummary) {
119
+ const tables = parseMarkdownTables(execSummary);
120
+ for (const table of tables) {
121
+ if (table.headers.some(h => h.toLowerCase().includes('condition'))) {
122
+ overview.killConditions = table.rows.map(row => ({
123
+ id: row['KC'] || row['#'] || '',
124
+ condition: row['Condition'] || '',
125
+ status: row['Status'] || 'UNTESTED',
126
+ evidence: row['Key Evidence'] || '',
127
+ }));
128
+ break;
129
+ }
130
+ }
131
+ }
132
+
133
+ return overview;
134
+ }
135
+
136
+ // ─── Build Entity Data (from CSV) ───────────────────────────────────────────
137
+
138
+ function buildEntities() {
139
+ console.log('Building entity data...');
140
+ const csvPath = PROJECT_CONFIG.pipelineSourceOfTruth || 'data/entities.csv';
141
+ const csv = readFile(csvPath);
142
+ if (!csv) {
143
+ console.warn(' No entity CSV found — skipping entity build');
144
+ return [];
145
+ }
146
+
147
+ try {
148
+ const records = parse(csv, { columns: true, skip_empty_lines: true, trim: true });
149
+ return records;
150
+ } catch (e) {
151
+ console.error(` ERROR parsing CSV: ${e.message}`);
152
+ return [];
153
+ }
154
+ }
155
+
156
+ // ─── Build Competitor Data ──────────────────────────────────────────────────
157
+
158
+ function buildCompetitors() {
159
+ console.log('Building competitor data...');
160
+ const researchMd = readFile('memory/research.md');
161
+ if (!researchMd) return [];
162
+
163
+ const tables = parseMarkdownTables(researchMd);
164
+ if (tables.length === 0) return [];
165
+
166
+ // Use the first table (capability map)
167
+ return tables[0].rows;
168
+ }
169
+
170
+ // ─── Build Decisions Data ────────────────────────────────────────────────────
171
+
172
+ function buildDecisions() {
173
+ console.log('Building decisions data...');
174
+ const md = readFile('memory/decisions.md');
175
+ if (!md) return [];
176
+
177
+ const decisions = [];
178
+ const decisionRegex = /### Decision (\d+):\s*(.+)/g;
179
+ let match;
180
+ const positions = [];
181
+
182
+ while ((match = decisionRegex.exec(md)) !== null) {
183
+ positions.push({
184
+ number: parseInt(match[1], 10),
185
+ title: match[2].trim(),
186
+ start: match.index + match[0].length,
187
+ });
188
+ }
189
+
190
+ if (positions.length === 0) return [];
191
+
192
+ for (let i = 0; i < positions.length; i++) {
193
+ const end = i + 1 < positions.length ? positions[i + 1].start - positions[i + 1].title.length - 20 : md.length;
194
+ const block = md.substring(positions[i].start, end);
195
+
196
+ const dateMatch = block.match(/\*\*Date\*\*:\s*(.+)/);
197
+ const decidedMatch = block.match(/\*\*Decided\*\*:\s*(.+)/);
198
+ const reversibilityMatch = block.match(/\*\*Reversibility\*\*:\s*(.+)/);
199
+ const revisitMatch = block.match(/\*\*Revisit trigger\*\*:\s*(.+)/);
200
+
201
+ decisions.push({
202
+ number: positions[i].number,
203
+ title: positions[i].title,
204
+ date: dateMatch ? dateMatch[1].trim() : '',
205
+ decided: decidedMatch ? decidedMatch[1].trim() : '',
206
+ reversibility: reversibilityMatch ? reversibilityMatch[1].trim() : '',
207
+ revisitTrigger: revisitMatch ? revisitMatch[1].trim() : '',
208
+ });
209
+ }
210
+
211
+ return decisions;
212
+ }
213
+
214
+ // ─── Build Scoring Data ─────────────────────────────────────────────────────
215
+
216
+ function buildScoring() {
217
+ console.log('Building scoring data...');
218
+ const md = readFile('memory/scoring.md');
219
+ if (!md) return { options: [], matrix: [], recommended: {} };
220
+
221
+ const tables = parseMarkdownTables(md);
222
+ const options = tables.length > 0 ? tables[0].rows : [];
223
+ const matrix = tables.length > 1 ? tables[1].rows : [];
224
+
225
+ // Extract Recommended Strategy section
226
+ const recommended = {};
227
+ const recMatch = md.match(/## Recommended Strategy([\s\S]*?)(?=\n## |\n# |$)/);
228
+ if (recMatch) {
229
+ const recBlock = recMatch[1];
230
+ const optionMatch = recBlock.match(/\*\*Option\*\*:\s*(.+)/);
231
+ const rationaleMatch = recBlock.match(/\*\*Rationale\*\*:\s*(.+)/);
232
+ const risksMatch = recBlock.match(/\*\*Risks\*\*:\s*(.+)/);
233
+ const fallbackMatch = recBlock.match(/\*\*Fallback\*\*:\s*(.+)/);
234
+
235
+ if (optionMatch) recommended.option = optionMatch[1].trim();
236
+ if (rationaleMatch) recommended.rationale = rationaleMatch[1].trim();
237
+ if (risksMatch) recommended.risks = risksMatch[1].trim();
238
+ if (fallbackMatch) recommended.fallback = fallbackMatch[1].trim();
239
+ }
240
+
241
+ return { options, matrix, recommended };
242
+ }
243
+
244
+ // ─── Build Timeline Data ────────────────────────────────────────────────────
245
+
246
+ function buildTimeline() {
247
+ console.log('Building timeline data...');
248
+ const md = readFile('docs/output/work-log.md');
249
+ if (!md) return [];
250
+
251
+ const tables = parseMarkdownTables(md);
252
+ if (tables.length === 0) return [];
253
+
254
+ // Find the table that has workstream-like headers
255
+ let workTable = null;
256
+ for (const table of tables) {
257
+ const lowerHeaders = table.headers.map(h => h.toLowerCase());
258
+ if (lowerHeaders.includes('workstream') || lowerHeaders.includes('item') || lowerHeaders.includes('date')) {
259
+ workTable = table;
260
+ break;
261
+ }
262
+ }
263
+ if (!workTable) workTable = tables[0];
264
+
265
+ const entries = workTable.rows.map(row => ({
266
+ workstream: row['Workstream'] || row['workstream'] || '',
267
+ item: row['Item'] || row['item'] || '',
268
+ owner: row['Owner'] || row['owner'] || '',
269
+ date: row['Date'] || row['date'] || '',
270
+ output: row['Output'] || row['output'] || '',
271
+ }));
272
+
273
+ // Sort by date descending
274
+ entries.sort((a, b) => {
275
+ if (!a.date && !b.date) return 0;
276
+ if (!a.date) return 1;
277
+ if (!b.date) return -1;
278
+ return new Date(b.date) - new Date(a.date);
279
+ });
280
+
281
+ return entries;
282
+ }
283
+
284
+ // ─── Build Research Data ────────────────────────────────────────────────────
285
+
286
+ function buildResearch() {
287
+ console.log('Building research data...');
288
+ const researchDir = path.join(PROJECT_ROOT, 'research');
289
+ const files = [];
290
+ const grades = { confirmed: 0, secondary: 0, inference: 0, assumption: 0 };
291
+
292
+ function scanDir(dir, relBase) {
293
+ let entries;
294
+ try {
295
+ entries = fs.readdirSync(dir, { withFileTypes: true });
296
+ } catch (e) {
297
+ console.warn(` WARN: Could not read directory ${dir}`);
298
+ return;
299
+ }
300
+ for (const entry of entries) {
301
+ const fullPath = path.join(dir, entry.name);
302
+ const relPath = path.join(relBase, entry.name);
303
+ if (entry.isDirectory()) {
304
+ scanDir(fullPath, relPath);
305
+ } else if (entry.name.endsWith('.md') && entry.name !== '.gitkeep') {
306
+ const content = fs.readFileSync(fullPath, 'utf-8');
307
+ const stat = fs.statSync(fullPath);
308
+
309
+ // Determine type from path
310
+ let type = 'other';
311
+ if (relPath.startsWith('competitors/') || relPath.startsWith('competitors\\')) type = 'competitor';
312
+ else if (relPath.startsWith('market/') || relPath.startsWith('market\\')) type = 'market';
313
+ else if (relPath.startsWith('technical/') || relPath.startsWith('technical\\')) type = 'technical';
314
+
315
+ // Entity name from filename slug
316
+ const entityName = path.basename(entry.name, '.md').replace(/[-_]/g, ' ');
317
+
318
+ // Check for TL;DR
319
+ const hasTldr = /## TL;DR|## tl;dr/i.test(content);
320
+
321
+ // Count evidence grades
322
+ const confirmed = (content.match(/\[CONFIRMED\]/g) || []).length;
323
+ const secondary = (content.match(/\[SECONDARY\]/g) || []).length;
324
+ const inference = (content.match(/\[INFERENCE\]/g) || []).length;
325
+ const assumption = (content.match(/\[ASSUMPTION\]/g) || []).length;
326
+
327
+ grades.confirmed += confirmed;
328
+ grades.secondary += secondary;
329
+ grades.inference += inference;
330
+ grades.assumption += assumption;
331
+
332
+ files.push({
333
+ filename: entry.name,
334
+ path: relPath,
335
+ type,
336
+ entityName,
337
+ hasTldr,
338
+ grades: { confirmed, secondary, inference, assumption },
339
+ lastModified: stat.mtime.toISOString(),
340
+ });
341
+ }
342
+ }
343
+ }
344
+
345
+ scanDir(researchDir, '');
346
+
347
+ // Identify gaps from memory/research.md capability map
348
+ const gaps = [];
349
+ const researchMd = readFile('memory/research.md');
350
+ if (researchMd) {
351
+ const tables = parseMarkdownTables(researchMd);
352
+ if (tables.length > 0) {
353
+ const entityNames = files.map(f => f.entityName.toLowerCase());
354
+ for (const row of tables[0].rows) {
355
+ // Check the first column as the entity identifier
356
+ const firstCol = Object.values(row)[0] || '';
357
+ if (firstCol && !entityNames.includes(firstCol.toLowerCase().replace(/[-_]/g, ' '))) {
358
+ gaps.push(firstCol);
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ return { files, grades, gaps };
365
+ }
366
+
367
+ // ─── Write Outputs ──────────────────────────────────────────────────────────
368
+
369
+ function writeJSON(filename, data) {
370
+ const outPath = path.join(OUTPUT_DIR, filename);
371
+ fs.writeFileSync(outPath, JSON.stringify(data, null, 2));
372
+ console.log(` ✓ ${filename} (${Array.isArray(data) ? data.length + ' items' : 'object'})`);
373
+ }
374
+
375
+ // ─── Main ───────────────────────────────────────────────────────────────────
376
+
377
+ console.log(`\nBuilding dashboard data for project at ${PROJECT_ROOT}\n`);
378
+
379
+ const overview = buildOverview();
380
+ const entities = buildEntities();
381
+ const competitors = buildCompetitors();
382
+ const decisions = buildDecisions();
383
+ const scoring = buildScoring();
384
+ const timeline = buildTimeline();
385
+ const research = buildResearch();
386
+
387
+ writeJSON('overview.json', overview);
388
+ writeJSON('entities.json', entities);
389
+ writeJSON('competitors.json', competitors);
390
+ writeJSON('decisions.json', decisions);
391
+ writeJSON('scoring.json', scoring);
392
+ writeJSON('timeline.json', timeline);
393
+ writeJSON('research.json', research);
394
+
395
+ console.log('\nDone.\n');
@@ -0,0 +1,143 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="robots" content="noindex, nofollow">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="generator" content="DS Strategy Stack by DiffTheEnder (github.com/DiffTheEnder/DSS-Claude-Stack)">
8
+ <title>Competitors — {{PROJECT_NAME}}</title>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Instrument+Serif&family=Syne:wght@600;700&display=swap" rel="stylesheet">
12
+ <script src="https://cdn.tailwindcss.com"></script>
13
+ <script>
14
+ tailwind.config = {
15
+ darkMode: 'class',
16
+ theme: { extend: {
17
+ colors: {
18
+ cream: { 50:"#F7F4EF",100:"#f5f2ed",200:"#efe9e0",300:"#e8e0d4",400:"#E3DDD4",white:"#FEFCF9" },
19
+ warmgray: { 100:"#f0ebe3",300:"#d4c8b8",400:"#9B9183",500:"#7D7265",600:"#5C5044",700:"#4a3c2e",900:"#2C2418" },
20
+ warmdark: { 950:"#1A1612",900:"#242018",800:"#2C2720",700:"#3A3530",600:"#4a3f32" },
21
+ accent: { 50:"#F2F7F5",100:"#E3EEEA",200:"#C5D9D4",400:"#2DB892",500:"#0E9C82",600:"#0E7C6B",700:"#0B6358",800:"#084C44",900:"#053B35" },
22
+ },
23
+ fontFamily: { sans:['DM Sans','system-ui','sans-serif'], serif:['Instrument Serif','Georgia','serif'], syne:['Syne','sans-serif'] }
24
+ }}
25
+ }
26
+ </script>
27
+ <link rel="stylesheet" href="css/styles.css">
28
+ </head>
29
+ <body class="bg-cream-50 dark:bg-warmdark-950 text-warmgray-900 dark:text-warmgray-100 min-h-screen flex">
30
+
31
+ <!-- Sidebar -->
32
+ <aside id="sidebar" class="sidebar flex flex-col shrink-0">
33
+ <a href="index.html" class="sidebar-brand" style="text-decoration:none;color:inherit">
34
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
35
+ <rect x="2" y="2" width="28" height="28" rx="8" stroke="rgba(255,255,255,0.15)" stroke-width="1.5"/>
36
+ <circle cx="16" cy="16" r="3" stroke="#2DB892" stroke-width="1.5"/>
37
+ <line x1="16" y1="6" x2="16" y2="12" stroke="#2DB892" stroke-width="1.5" stroke-linecap="round"/>
38
+ <line x1="16" y1="20" x2="16" y2="26" stroke="#2DB892" stroke-width="1.5" stroke-linecap="round"/>
39
+ <line x1="6" y1="16" x2="12" y2="16" stroke="#2DB892" stroke-width="1.5" stroke-linecap="round"/>
40
+ <line x1="20" y1="16" x2="26" y2="16" stroke="#2DB892" stroke-width="1.5" stroke-linecap="round"/>
41
+ </svg>
42
+ <div>
43
+ <div class="sidebar-title">{{PROJECT_NAME}}</div>
44
+ <div class="sidebar-subtitle">Strategy Dashboard</div>
45
+ </div>
46
+ </a>
47
+ <nav class="sidebar-nav">
48
+ <div class="sidebar-label">Core</div>
49
+ <a href="index.html" class="nav-link">
50
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="8" cy="8" r="6"/><path d="M8 5v3l2 2"/></svg>
51
+ Overview
52
+ </a>
53
+ <a href="pipeline.html" class="nav-link">
54
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 4h12M2 8h8M2 12h5"/></svg>
55
+ Pipeline
56
+ </a>
57
+ <a href="competitors.html" class="nav-link">
58
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></svg>
59
+ Competitors
60
+ </a>
61
+ <div class="sidebar-label" style="margin-top: 0.75rem">Strategy</div>
62
+ <a href="decisions.html" class="nav-link">
63
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M8 2v12M2 8h12"/><circle cx="8" cy="8" r="6"/></svg>
64
+ Decisions
65
+ </a>
66
+ <a href="scoring.html" class="nav-link">
67
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M3 13l3-4 3 2 4-6"/></svg>
68
+ Scoring
69
+ </a>
70
+ <a href="timeline.html" class="nav-link">
71
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 3h12M2 8h12M2 13h12"/><circle cx="5" cy="3" r="1" fill="currentColor"/><circle cx="10" cy="8" r="1" fill="currentColor"/><circle cx="7" cy="13" r="1" fill="currentColor"/></svg>
72
+ Timeline
73
+ </a>
74
+ <a href="research.html" class="nav-link">
75
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="7" cy="7" r="5"/><path d="M11 11l3 3"/></svg>
76
+ Research Hub
77
+ </a>
78
+ </nav>
79
+ <div style="padding: 0.75rem; border-top: 1px solid rgba(255,248,240,0.06);">
80
+ <button id="dark-toggle" class="nav-link" style="width:100%; border:none; background:none; cursor:pointer">
81
+ <svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M8 1a7 7 0 100 14 5 5 0 010-10"/></svg>Toggle Theme
82
+ </button>
83
+ </div>
84
+ <div style="padding: 0.5rem 0.75rem; border-top: 1px solid rgba(255,248,240,0.06); text-align: center;">
85
+ <a href="https://github.com/DiffTheEnder/DSS-Claude-Stack" target="_blank" rel="noopener" style="font-size: 0.65rem; color: rgba(155,145,131,0.5); text-decoration: none;">Powered by DS Strategy Stack</a>
86
+ </div>
87
+ </aside>
88
+
89
+ <!-- Main Content -->
90
+ <main class="flex-1 ml-[240px] p-8">
91
+ <header class="mb-8">
92
+ <h1 class="font-serif text-3xl mb-2">Competitive Landscape</h1>
93
+ <p class="text-warmgray-400 text-sm">Data from <code>memory/research.md</code> capability map</p>
94
+ </header>
95
+
96
+ <section class="bg-white dark:bg-warmdark-800 rounded-xl border border-cream-300 dark:border-warmdark-700 overflow-x-auto">
97
+ <table class="w-full text-sm">
98
+ <thead id="comp-head">
99
+ <!-- Populated by JS -->
100
+ </thead>
101
+ <tbody id="comp-body">
102
+ <!-- Populated by JS -->
103
+ </tbody>
104
+ </table>
105
+ </section>
106
+ </main>
107
+
108
+ <script src="js/app.js"></script>
109
+ <script>
110
+ // Competitors page logic — loads competitors.json
111
+ document.addEventListener('DOMContentLoaded', async () => {
112
+ initDarkMode();
113
+ initSidebar();
114
+ initNav();
115
+
116
+ try {
117
+ const competitors = await loadJSON('competitors.json');
118
+ const thead = document.getElementById('comp-head');
119
+ const tbody = document.getElementById('comp-body');
120
+
121
+ if (competitors.length === 0) {
122
+ tbody.innerHTML = '<tr><td colspan="5" class="p-6 text-center text-warmgray-400">No competitor data yet. Add entries to <code>memory/research.md</code> capability map.</td></tr>';
123
+ return;
124
+ }
125
+
126
+ // Build headers from first row keys
127
+ const keys = Object.keys(competitors[0]);
128
+ thead.innerHTML = '<tr class="border-b border-cream-300 dark:border-warmdark-700">' +
129
+ keys.map(k => `<th class="text-left p-3 font-semibold">${k}</th>`).join('') + '</tr>';
130
+
131
+ competitors.forEach(comp => {
132
+ const row = document.createElement('tr');
133
+ row.className = 'border-b border-cream-200 dark:border-warmdark-700 hover:bg-cream-100 dark:hover:bg-warmdark-700 transition-colors';
134
+ row.innerHTML = keys.map(k => `<td class="p-3">${comp[k] || '—'}</td>`).join('');
135
+ tbody.appendChild(row);
136
+ });
137
+ } catch (e) {
138
+ console.warn('Could not load competitor data:', e.message);
139
+ }
140
+ });
141
+ </script>
142
+ </body>
143
+ </html>
@@ -0,0 +1,143 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Strategy Dashboard — Design System
3
+ Warm cream/teal editorial palette
4
+ ═══════════════════════════════════════════════ */
5
+
6
+ /* ── Sidebar ── */
7
+ .sidebar {
8
+ width: 240px;
9
+ height: 100vh;
10
+ background: #1C1713;
11
+ position: fixed;
12
+ top: 0;
13
+ left: 0;
14
+ overflow-y: auto;
15
+ transition: transform 0.2s;
16
+ z-index: 30;
17
+ }
18
+ .sidebar::after {
19
+ content: '';
20
+ position: absolute;
21
+ inset: 0;
22
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
23
+ background-size: 128px 128px;
24
+ pointer-events: none;
25
+ z-index: 1;
26
+ }
27
+ .sidebar > * { position: relative; z-index: 2; }
28
+ .sidebar-brand {
29
+ display: flex;
30
+ align-items: center;
31
+ padding: 1.25rem;
32
+ gap: 0.75rem;
33
+ border-bottom: 1px solid rgba(255,248,240,0.06);
34
+ }
35
+ .sidebar-title {
36
+ font-family: 'Syne', sans-serif;
37
+ font-size: 1rem;
38
+ font-weight: 700;
39
+ color: white;
40
+ letter-spacing: 0.01em;
41
+ line-height: 1.2;
42
+ }
43
+ .sidebar-subtitle {
44
+ font-size: 0.6rem;
45
+ text-transform: uppercase;
46
+ letter-spacing: 0.1em;
47
+ color: rgba(255,248,240,0.45);
48
+ }
49
+ .sidebar-nav {
50
+ flex: 1;
51
+ padding: 0.75rem;
52
+ overflow-y: auto;
53
+ }
54
+ .sidebar-label {
55
+ font-size: 0.6rem;
56
+ text-transform: uppercase;
57
+ letter-spacing: 0.1em;
58
+ color: rgba(255,248,240,0.40);
59
+ padding-left: 0.75rem;
60
+ margin-top: 1.25rem;
61
+ margin-bottom: 0.25rem;
62
+ }
63
+ .sidebar-label:first-child { margin-top: 0; }
64
+ .nav-link {
65
+ font-size: 0.8125rem;
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 0.625rem;
69
+ padding: 0.5rem 0.75rem;
70
+ border-radius: 0.375rem;
71
+ color: rgba(255,248,240,0.70);
72
+ transition: background 0.15s, color 0.15s;
73
+ position: relative;
74
+ text-decoration: none;
75
+ }
76
+ .nav-link:hover {
77
+ background: rgba(255,248,240,0.05);
78
+ color: rgba(255,248,240,0.85);
79
+ }
80
+ .nav-link.active {
81
+ background: rgba(14,124,107,0.12);
82
+ color: white;
83
+ }
84
+ .nav-link.active::before {
85
+ content: '';
86
+ position: absolute;
87
+ left: 0;
88
+ top: 0.375rem;
89
+ bottom: 0.375rem;
90
+ width: 2px;
91
+ border-radius: 1px;
92
+ background: #2DB892;
93
+ }
94
+ .nav-icon {
95
+ width: 16px;
96
+ height: 16px;
97
+ flex-shrink: 0;
98
+ }
99
+
100
+ /* ── Kill Condition Cards ── */
101
+ .kc-card {
102
+ background: white;
103
+ border: 1px solid #e8e0d4;
104
+ border-radius: 0.75rem;
105
+ padding: 1.25rem;
106
+ transition: box-shadow 0.2s;
107
+ }
108
+ .dark .kc-card {
109
+ background: #2C2720;
110
+ border-color: #3A3530;
111
+ }
112
+ .kc-card:hover {
113
+ box-shadow: 0 4px 12px rgba(0,0,0,0.06);
114
+ }
115
+ .kc-badge {
116
+ display: inline-block;
117
+ padding: 0.125rem 0.5rem;
118
+ border-radius: 0.25rem;
119
+ font-size: 0.6875rem;
120
+ font-weight: 600;
121
+ text-transform: uppercase;
122
+ letter-spacing: 0.03em;
123
+ }
124
+ .kc-untested { background: #f0ebe3; color: #7D7265; }
125
+ .kc-early-signal { background: #FEF3C7; color: #92400E; }
126
+ .kc-building { background: #DBEAFE; color: #1E40AF; }
127
+ .kc-weakening { background: #FEE2E2; color: #991B1B; }
128
+ .kc-passed { background: #D1FAE5; color: #065F46; }
129
+ .kc-failed { background: #FEE2E2; color: #991B1B; }
130
+
131
+ .dark .kc-untested { background: rgba(125,114,101,0.2); color: #d4c8b8; }
132
+ .dark .kc-early-signal { background: rgba(245,158,11,0.15); color: #fbbf24; }
133
+ .dark .kc-building { background: rgba(59,130,246,0.15); color: #93C5FD; }
134
+ .dark .kc-weakening { background: rgba(239,68,68,0.15); color: #FCA5A5; }
135
+ .dark .kc-passed { background: rgba(16,185,129,0.15); color: #6EE7B7; }
136
+ .dark .kc-failed { background: rgba(239,68,68,0.15); color: #FCA5A5; }
137
+
138
+ /* ── Responsive ── */
139
+ @media (max-width: 768px) {
140
+ .sidebar { transform: translateX(-100%); }
141
+ .sidebar.open { transform: translateX(0); }
142
+ main { margin-left: 0 !important; }
143
+ }
File without changes