@leanspec/ui 0.2.3 → 0.2.4
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/standalone/packages/web/.next/BUILD_ID +1 -1
- package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
- package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
- package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
- package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
- package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
- package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
- package/dist/standalone/packages/web/components.json +20 -0
- package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
- package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
- package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
- package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
- package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
- package/dist/standalone/packages/web/drizzle.config.ts +10 -0
- package/dist/standalone/packages/web/eslint.config.mjs +18 -0
- package/dist/standalone/packages/web/next.config.ts +7 -0
- package/dist/standalone/packages/web/package.json +1 -1
- package/dist/standalone/packages/web/postcss.config.mjs +8 -0
- package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
- package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
- package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
- package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
- package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
- package/dist/standalone/packages/web/src/app/error.tsx +43 -0
- package/dist/standalone/packages/web/src/app/globals.css +531 -0
- package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
- package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
- package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
- package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
- package/dist/standalone/packages/web/src/app/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
- package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
- package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
- package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
- package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
- package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
- package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
- package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
- package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
- package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
- package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
- package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
- package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
- package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
- package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
- package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
- package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
- package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
- package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
- package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
- package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
- package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
- package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
- package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
- package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
- package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
- package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
- package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
- package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
- package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
- package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
- package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
- package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
- package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
- package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
- package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
- package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
- package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
- package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
- package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
- package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
- package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
- package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
- package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
- package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
- package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
- package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
- package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
- package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
- package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
- package/dist/standalone/packages/web/src/types/specs.ts +18 -0
- package/dist/standalone/packages/web/tailwind.config.ts +58 -0
- package/dist/standalone/packages/web/tsconfig.json +34 -0
- package/dist/standalone/packages/web/vitest.config.ts +14 -0
- package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
- package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
- package/package.json +5 -3
- package/dist/BUILD_ID +0 -1
- package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
- /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
- /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
- /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/window.svg +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-based data access functions
|
|
3
|
+
* These replace direct database queries with the unified specs service
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { specsService } from '../specs/service';
|
|
7
|
+
import type { Spec } from './schema';
|
|
8
|
+
import { detectSubSpecs } from '../sub-specs';
|
|
9
|
+
import { join, resolve } from 'path';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import matter from 'gray-matter';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Spec with parsed tags (for client consumption)
|
|
15
|
+
*/
|
|
16
|
+
export type ParsedSpec = Omit<Spec, 'tags'> & {
|
|
17
|
+
tags: string[] | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SPECS_DIR = resolve(process.cwd(), '../../specs');
|
|
21
|
+
|
|
22
|
+
function getSpecsRootDir(): string {
|
|
23
|
+
const envDir = process.env.SPECS_DIR;
|
|
24
|
+
if (!envDir) {
|
|
25
|
+
return DEFAULT_SPECS_DIR;
|
|
26
|
+
}
|
|
27
|
+
return envDir.startsWith('/') ? envDir : resolve(process.cwd(), envDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildSpecDirPath(filePath: string): string {
|
|
31
|
+
const normalized = filePath
|
|
32
|
+
.replace(/^specs\//, '')
|
|
33
|
+
.replace(/\/README\.md$/, '');
|
|
34
|
+
return join(getSpecsRootDir(), normalized);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SpecRelationships {
|
|
38
|
+
dependsOn: string[];
|
|
39
|
+
related: string[];
|
|
40
|
+
}
|
|
41
|
+
function normalizeRelationshipList(value: unknown): string[] {
|
|
42
|
+
if (!value) return [];
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return value.map((entry) => String(entry));
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
return [value];
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getFilesystemRelationships(specDirPath: string): SpecRelationships {
|
|
53
|
+
try {
|
|
54
|
+
const readmePath = join(specDirPath, 'README.md');
|
|
55
|
+
const raw = readFileSync(readmePath, 'utf-8');
|
|
56
|
+
const { data } = matter(raw);
|
|
57
|
+
const dependsOn = normalizeRelationshipList(data?.depends_on ?? data?.dependsOn);
|
|
58
|
+
const related = normalizeRelationshipList(data?.related);
|
|
59
|
+
return {
|
|
60
|
+
dependsOn,
|
|
61
|
+
related,
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn('Unable to parse spec relationships', error);
|
|
65
|
+
return { dependsOn: [], related: [] };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse tags from JSON string to array
|
|
71
|
+
*/
|
|
72
|
+
function parseSpecTags(spec: Spec): ParsedSpec {
|
|
73
|
+
return {
|
|
74
|
+
...spec,
|
|
75
|
+
tags: spec.tags ? (typeof spec.tags === 'string' ? JSON.parse(spec.tags) : spec.tags) : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Count sub-specs in a directory
|
|
81
|
+
*/
|
|
82
|
+
function countSubSpecs(specDirPath: string): number {
|
|
83
|
+
try {
|
|
84
|
+
const { readdirSync, existsSync, statSync } = require('fs');
|
|
85
|
+
if (!existsSync(specDirPath)) return 0;
|
|
86
|
+
|
|
87
|
+
const entries = readdirSync(specDirPath);
|
|
88
|
+
let count = 0;
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
// Skip README.md (main spec file) and non-.md files
|
|
92
|
+
if (entry === 'README.md' || !entry.endsWith('.md')) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const filePath = join(specDirPath, entry);
|
|
97
|
+
try {
|
|
98
|
+
const stat = statSync(filePath);
|
|
99
|
+
if (stat.isFile()) {
|
|
100
|
+
count++;
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Skip files that can't be accessed
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return count;
|
|
108
|
+
} catch {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get all specs (uses filesystem by default, database if projectId provided)
|
|
115
|
+
*/
|
|
116
|
+
export async function getSpecs(projectId?: string): Promise<ParsedSpec[]> {
|
|
117
|
+
const specs = await specsService.getAllSpecs(projectId);
|
|
118
|
+
return specs.map(parseSpecTags);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get all specs with sub-spec count (for sidebar)
|
|
123
|
+
*/
|
|
124
|
+
export async function getSpecsWithSubSpecCount(projectId?: string): Promise<(ParsedSpec & { subSpecsCount: number })[]> {
|
|
125
|
+
const specs = await specsService.getAllSpecs(projectId);
|
|
126
|
+
|
|
127
|
+
// Only count sub-specs for filesystem mode
|
|
128
|
+
if (projectId) {
|
|
129
|
+
return specs.map(spec => ({ ...parseSpecTags(spec), subSpecsCount: 0 }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return specs.map(spec => {
|
|
133
|
+
const specDirPath = buildSpecDirPath(spec.filePath);
|
|
134
|
+
const subSpecsCount = countSubSpecs(specDirPath);
|
|
135
|
+
return { ...parseSpecTags(spec), subSpecsCount };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get a spec by ID (number or UUID)
|
|
141
|
+
*/
|
|
142
|
+
export async function getSpecById(id: string, projectId?: string): Promise<(ParsedSpec & { subSpecs?: import('../sub-specs').SubSpec[]; relationships?: SpecRelationships }) | null> {
|
|
143
|
+
const spec = await specsService.getSpec(id, projectId);
|
|
144
|
+
|
|
145
|
+
if (!spec) return null;
|
|
146
|
+
|
|
147
|
+
const parsedSpec = parseSpecTags(spec);
|
|
148
|
+
|
|
149
|
+
// Detect sub-specs from filesystem (only for filesystem mode)
|
|
150
|
+
if (!projectId) {
|
|
151
|
+
const specDirPath = buildSpecDirPath(spec.filePath);
|
|
152
|
+
const subSpecs = detectSubSpecs(specDirPath);
|
|
153
|
+
const relationships = getFilesystemRelationships(specDirPath);
|
|
154
|
+
return { ...parsedSpec, subSpecs, relationships };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return parsedSpec;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get specs by status
|
|
162
|
+
*/
|
|
163
|
+
export async function getSpecsByStatus(
|
|
164
|
+
status: 'planned' | 'in-progress' | 'complete' | 'archived',
|
|
165
|
+
projectId?: string
|
|
166
|
+
): Promise<ParsedSpec[]> {
|
|
167
|
+
const specs = await specsService.getSpecsByStatus(status, projectId);
|
|
168
|
+
return specs.map(parseSpecTags);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Search specs
|
|
173
|
+
*/
|
|
174
|
+
export async function searchSpecs(query: string, projectId?: string): Promise<ParsedSpec[]> {
|
|
175
|
+
const specs = await specsService.searchSpecs(query, projectId);
|
|
176
|
+
return specs.map(parseSpecTags);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Stats calculation
|
|
181
|
+
*/
|
|
182
|
+
export interface StatsResult {
|
|
183
|
+
totalProjects: number;
|
|
184
|
+
totalSpecs: number;
|
|
185
|
+
specsByStatus: { status: string; count: number }[];
|
|
186
|
+
specsByPriority: { priority: string; count: number }[];
|
|
187
|
+
completionRate: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function getStats(projectId?: string): Promise<StatsResult> {
|
|
191
|
+
const specs = await specsService.getAllSpecs(projectId);
|
|
192
|
+
|
|
193
|
+
// Count by status
|
|
194
|
+
const statusCounts = new Map<string, number>();
|
|
195
|
+
const priorityCounts = new Map<string, number>();
|
|
196
|
+
|
|
197
|
+
for (const spec of specs) {
|
|
198
|
+
if (spec.status) {
|
|
199
|
+
statusCounts.set(spec.status, (statusCounts.get(spec.status) || 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
if (spec.priority) {
|
|
202
|
+
priorityCounts.set(spec.priority, (priorityCounts.get(spec.priority) || 0) + 1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const completeCount = statusCounts.get('complete') || 0;
|
|
207
|
+
const completionRate = specs.length > 0 ? (completeCount / specs.length) * 100 : 0;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
totalProjects: 1, // For now, single project (LeanSpec)
|
|
211
|
+
totalSpecs: specs.length,
|
|
212
|
+
specsByStatus: Array.from(statusCounts.entries()).map(([status, count]) => ({ status, count })),
|
|
213
|
+
specsByPriority: Array.from(priorityCounts.entries()).map(([priority, count]) => ({ priority, count })),
|
|
214
|
+
completionRate: Math.round(completionRate * 10) / 10,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Graph for Web API
|
|
3
|
+
* Standalone implementation to avoid tiktoken wasm issues from @leanspec/core
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SpecFrontmatter {
|
|
7
|
+
status: string;
|
|
8
|
+
created: string;
|
|
9
|
+
depends_on?: string[];
|
|
10
|
+
related?: string[];
|
|
11
|
+
priority?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
assignee?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SpecInfo {
|
|
17
|
+
path: string;
|
|
18
|
+
fullPath: string;
|
|
19
|
+
filePath: string;
|
|
20
|
+
name: string;
|
|
21
|
+
frontmatter: SpecFrontmatter;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DependencyNode {
|
|
25
|
+
dependsOn: Set<string>;
|
|
26
|
+
requiredBy: Set<string>;
|
|
27
|
+
related: Set<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CompleteDependencyGraph {
|
|
31
|
+
current: SpecInfo;
|
|
32
|
+
dependsOn: SpecInfo[];
|
|
33
|
+
requiredBy: SpecInfo[];
|
|
34
|
+
related: SpecInfo[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Dependency graph builder
|
|
39
|
+
*/
|
|
40
|
+
export class SpecDependencyGraph {
|
|
41
|
+
private graph: Map<string, DependencyNode>;
|
|
42
|
+
private specs: Map<string, SpecInfo>;
|
|
43
|
+
|
|
44
|
+
constructor(allSpecs: SpecInfo[]) {
|
|
45
|
+
this.graph = new Map();
|
|
46
|
+
this.specs = new Map();
|
|
47
|
+
this.buildGraph(allSpecs);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private buildGraph(specs: SpecInfo[]): void {
|
|
51
|
+
// First pass: Initialize all nodes
|
|
52
|
+
for (const spec of specs) {
|
|
53
|
+
this.specs.set(spec.path, spec);
|
|
54
|
+
this.graph.set(spec.path, {
|
|
55
|
+
dependsOn: new Set(spec.frontmatter.depends_on || []),
|
|
56
|
+
requiredBy: new Set(),
|
|
57
|
+
related: new Set(spec.frontmatter.related || []),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Second pass: Build reverse edges
|
|
62
|
+
for (const [specPath, node] of this.graph.entries()) {
|
|
63
|
+
// For each dependsOn, add reverse requiredBy edge
|
|
64
|
+
for (const dep of node.dependsOn) {
|
|
65
|
+
const depNode = this.graph.get(dep);
|
|
66
|
+
if (depNode) {
|
|
67
|
+
depNode.requiredBy.add(specPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For each related, add bidirectional link
|
|
72
|
+
for (const rel of node.related) {
|
|
73
|
+
const relNode = this.graph.get(rel);
|
|
74
|
+
if (relNode) {
|
|
75
|
+
relNode.related.add(specPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getCompleteGraph(specPath: string): CompleteDependencyGraph {
|
|
82
|
+
const spec = this.specs.get(specPath);
|
|
83
|
+
if (!spec) {
|
|
84
|
+
throw new Error(`Spec not found: ${specPath}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const node = this.graph.get(specPath);
|
|
88
|
+
if (!node) {
|
|
89
|
+
throw new Error(`Graph node not found: ${specPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
current: spec,
|
|
94
|
+
dependsOn: this.getSpecsByPaths(Array.from(node.dependsOn)),
|
|
95
|
+
requiredBy: this.getSpecsByPaths(Array.from(node.requiredBy)),
|
|
96
|
+
related: this.getSpecsByPaths(Array.from(node.related)),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private getSpecsByPaths(paths: string[]): SpecInfo[] {
|
|
101
|
+
return paths
|
|
102
|
+
.map(path => this.specs.get(path))
|
|
103
|
+
.filter((spec): spec is SpecInfo => spec !== undefined);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified specs service
|
|
3
|
+
* Routes requests to filesystem or database source based on configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FilesystemSource } from './sources/filesystem-source';
|
|
7
|
+
import type { SpecSource } from './types';
|
|
8
|
+
import type { Spec } from '../db/schema';
|
|
9
|
+
|
|
10
|
+
// Lazy import DatabaseSource to avoid DB connection issues in filesystem mode
|
|
11
|
+
type DatabaseSourceConstructor = new () => SpecSource;
|
|
12
|
+
let DatabaseSourceCtor: DatabaseSourceConstructor | null = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service modes
|
|
16
|
+
* - filesystem: Read from local filesystem only (LeanSpec's own specs)
|
|
17
|
+
* - database: Read from database only (external repos)
|
|
18
|
+
* - both: Support both modes (route based on projectId)
|
|
19
|
+
*/
|
|
20
|
+
type SpecsMode = 'filesystem' | 'database' | 'both';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unified specs service
|
|
24
|
+
* Provides a single interface for accessing specs from different sources
|
|
25
|
+
*/
|
|
26
|
+
export class SpecsService {
|
|
27
|
+
private filesystemSource?: FilesystemSource;
|
|
28
|
+
private databaseSource?: SpecSource;
|
|
29
|
+
private mode: SpecsMode;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.mode = (process.env.SPECS_MODE as SpecsMode) || 'filesystem';
|
|
33
|
+
|
|
34
|
+
if (this.mode === 'filesystem' || this.mode === 'both') {
|
|
35
|
+
const specsDir = process.env.SPECS_DIR;
|
|
36
|
+
this.filesystemSource = new FilesystemSource(specsDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Don't instantiate database source here - do it lazily
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lazy load database source
|
|
44
|
+
*/
|
|
45
|
+
private async getDatabaseSource(): Promise<SpecSource> {
|
|
46
|
+
if (!this.databaseSource) {
|
|
47
|
+
if (!DatabaseSourceCtor) {
|
|
48
|
+
const { DatabaseSource } = await import('./sources/database-source');
|
|
49
|
+
DatabaseSourceCtor = DatabaseSource;
|
|
50
|
+
}
|
|
51
|
+
this.databaseSource = new DatabaseSourceCtor();
|
|
52
|
+
}
|
|
53
|
+
return this.databaseSource;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all specs
|
|
58
|
+
* If projectId is provided, uses database source (external repo)
|
|
59
|
+
* Otherwise uses filesystem source (LeanSpec's own specs)
|
|
60
|
+
*/
|
|
61
|
+
async getAllSpecs(projectId?: string): Promise<Spec[]> {
|
|
62
|
+
const source = await this.getSource(projectId);
|
|
63
|
+
return await source.getAllSpecs(projectId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get a single spec
|
|
68
|
+
*/
|
|
69
|
+
async getSpec(specPath: string, projectId?: string): Promise<Spec | null> {
|
|
70
|
+
const source = await this.getSource(projectId);
|
|
71
|
+
return await source.getSpec(specPath, projectId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get specs by status
|
|
76
|
+
*/
|
|
77
|
+
async getSpecsByStatus(
|
|
78
|
+
status: 'planned' | 'in-progress' | 'complete' | 'archived',
|
|
79
|
+
projectId?: string
|
|
80
|
+
): Promise<Spec[]> {
|
|
81
|
+
const source = await this.getSource(projectId);
|
|
82
|
+
return await source.getSpecsByStatus(status, projectId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Search specs
|
|
87
|
+
*/
|
|
88
|
+
async searchSpecs(query: string, projectId?: string): Promise<Spec[]> {
|
|
89
|
+
const source = await this.getSource(projectId);
|
|
90
|
+
return await source.searchSpecs(query, projectId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Invalidate cache
|
|
95
|
+
*/
|
|
96
|
+
async invalidateCache(specPath?: string, projectId?: string): Promise<void> {
|
|
97
|
+
const source = await this.getSource(projectId);
|
|
98
|
+
source.invalidateCache(specPath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the appropriate source based on projectId and mode
|
|
103
|
+
*/
|
|
104
|
+
private async getSource(projectId?: string): Promise<SpecSource> {
|
|
105
|
+
// If projectId provided, use database (external repo)
|
|
106
|
+
if (projectId && (this.mode === 'database' || this.mode === 'both')) {
|
|
107
|
+
return await this.getDatabaseSource();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Otherwise use filesystem (LeanSpec's own specs)
|
|
111
|
+
if (this.filesystemSource) {
|
|
112
|
+
return this.filesystemSource;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error('No spec source configured');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Singleton instance
|
|
120
|
+
export const specsService = new SpecsService();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database-backed spec source
|
|
3
|
+
* Reads specs from PostgreSQL/SQLite database
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db, schema } from '../../db';
|
|
7
|
+
import { eq, and } from 'drizzle-orm';
|
|
8
|
+
import type { SpecSource } from '../types';
|
|
9
|
+
import type { Spec } from '../../db/schema';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Database source implementation
|
|
13
|
+
* Reads specs from database (for external GitHub repos)
|
|
14
|
+
*/
|
|
15
|
+
export class DatabaseSource implements SpecSource {
|
|
16
|
+
/**
|
|
17
|
+
* Get all specs, optionally filtered by projectId
|
|
18
|
+
*/
|
|
19
|
+
async getAllSpecs(projectId?: string): Promise<Spec[]> {
|
|
20
|
+
const query = projectId
|
|
21
|
+
? db.select().from(schema.specs).where(eq(schema.specs.projectId, projectId))
|
|
22
|
+
: db.select().from(schema.specs);
|
|
23
|
+
|
|
24
|
+
return await query.orderBy(schema.specs.specNumber);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get a single spec by path and projectId
|
|
29
|
+
*/
|
|
30
|
+
async getSpec(specPath: string, projectId?: string): Promise<Spec | null> {
|
|
31
|
+
if (!projectId) {
|
|
32
|
+
return null; // Database mode requires projectId
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse spec number from path (e.g., "035" or "035-my-spec")
|
|
36
|
+
const specNum = parseInt(specPath.split('-')[0], 10);
|
|
37
|
+
if (isNaN(specNum)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const results = await db
|
|
42
|
+
.select()
|
|
43
|
+
.from(schema.specs)
|
|
44
|
+
.where(and(eq(schema.specs.projectId, projectId), eq(schema.specs.specNumber, specNum)))
|
|
45
|
+
.limit(1);
|
|
46
|
+
|
|
47
|
+
return results.length > 0 ? results[0] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get specs by status
|
|
52
|
+
*/
|
|
53
|
+
async getSpecsByStatus(
|
|
54
|
+
status: 'planned' | 'in-progress' | 'complete' | 'archived',
|
|
55
|
+
projectId?: string
|
|
56
|
+
): Promise<Spec[]> {
|
|
57
|
+
const conditions = [eq(schema.specs.status, status)];
|
|
58
|
+
if (projectId) {
|
|
59
|
+
conditions.push(eq(schema.specs.projectId, projectId));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return await db
|
|
63
|
+
.select()
|
|
64
|
+
.from(schema.specs)
|
|
65
|
+
.where(and(...conditions))
|
|
66
|
+
.orderBy(schema.specs.specNumber);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Search specs by query
|
|
71
|
+
*/
|
|
72
|
+
async searchSpecs(query: string, projectId?: string): Promise<Spec[]> {
|
|
73
|
+
// TODO: Implement full-text search with database
|
|
74
|
+
// For now, just return all specs and filter in memory
|
|
75
|
+
const allSpecs = await this.getAllSpecs(projectId);
|
|
76
|
+
const lowerQuery = query.toLowerCase();
|
|
77
|
+
|
|
78
|
+
return allSpecs.filter((spec) => {
|
|
79
|
+
return (
|
|
80
|
+
spec.specName.toLowerCase().includes(lowerQuery) ||
|
|
81
|
+
spec.title?.toLowerCase().includes(lowerQuery) ||
|
|
82
|
+
spec.contentMd.toLowerCase().includes(lowerQuery)
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Invalidate cache (no-op for database source)
|
|
89
|
+
*/
|
|
90
|
+
invalidateCache(): void {
|
|
91
|
+
// Database source doesn't have its own cache
|
|
92
|
+
// Next.js handles caching at the page level
|
|
93
|
+
}
|
|
94
|
+
}
|