attempt to use separate folder for docs and meetings

This commit is contained in:
Mariano Gabriel
2026-07-06 01:18:17 -03:00
parent bb84558526
commit 31ad8e5928
10 changed files with 261 additions and 62 deletions

View File

@@ -26,7 +26,7 @@ import path from 'node:path'
interface Options {
outputDirs: string[] // explicit output dirs (env)
outputsRoot: string // managed root: UI-scanned sources land here
outputsRoots: string[] // managed roots (doocus-data, meetus-data): <root>/<source>/
repoRoot: string // where process_tree.py lives (run via uv)
}
@@ -82,17 +82,23 @@ function buildCollections(dirs: string[]): Collection[] {
}
export function doocusApi(opts: Options): Plugin {
/** Collections discovered under the managed root (one subdir per source). */
function discoverInRoot(): Collection[] {
let subs: fs.Dirent[] = []
try { subs = fs.readdirSync(opts.outputsRoot, { withFileTypes: true }) } catch { return [] }
return subs
.filter((d) => d.isDirectory() && fs.existsSync(path.join(opts.outputsRoot, d.name, 'index.json')))
.map((d) => ({
id: d.name, name: d.name,
dir: path.join(opts.outputsRoot, d.name),
indexPath: path.join(opts.outputsRoot, d.name, 'index.json'),
}))
/** Collections discovered across the managed roots (each is <root>/<source>/). */
function discoverInRoots(): Collection[] {
const found: Collection[] = []
for (const root of opts.outputsRoots) {
let subs: fs.Dirent[] = []
try { subs = fs.readdirSync(root, { withFileTypes: true }) } catch { continue }
for (const d of subs) {
if (d.isDirectory() && fs.existsSync(path.join(root, d.name, 'index.json'))) {
found.push({
id: d.name, name: d.name,
dir: path.join(root, d.name),
indexPath: path.join(root, d.name, 'index.json'),
})
}
}
}
return found
}
/** Current collections = explicit env dirs + managed-root subdirs (deduped,
@@ -100,7 +106,7 @@ export function doocusApi(opts: Options): Plugin {
function currentCollections(): Collection[] {
const out: Collection[] = []
const seen = new Set<string>()
for (const c of [...buildCollections(opts.outputDirs), ...discoverInRoot()]) {
for (const c of [...buildCollections(opts.outputDirs), ...discoverInRoots()]) {
if (out.some((o) => o.dir === c.dir)) continue
let id = c.id, n = 1
while (seen.has(id)) { n++; id = `${c.id}-${n}` }
@@ -172,7 +178,7 @@ export function doocusApi(opts: Options): Plugin {
configureServer(server) {
const cols = currentCollections()
server.config.logger.info(
`[doocus-api] ${cols.length} collection(s); managed root: ${opts.outputsRoot}`)
`[doocus-api] ${cols.length} collection(s); managed roots: ${opts.outputsRoots.join(', ')}`)
server.middlewares.use('/api', (req, res, next) => {
const url = new URL(req.url ?? '/', 'http://localhost')
const parts = url.pathname.split('/').filter(Boolean)
@@ -205,7 +211,14 @@ export function doocusApi(opts: Options): Plugin {
function listCollections(res: ServerResponse): void {
const out = currentCollections().map((c) => {
const idx = readIndexFor(c)
return { id: c.id, name: c.name, available: !!idx, count: idx?.files.length ?? 0 }
const meetings = idx?.files.filter((f) => f.mode === 'meeting').length ?? 0
return {
id: c.id, name: c.name, available: !!idx,
count: idx?.files.length ?? 0,
root: idx?.root ?? null, // shared root → same source
only: (idx as any)?.only ?? 'all',
meetings,
}
})
sendJson(res, 200, { collections: out })
}
@@ -218,32 +231,35 @@ export function doocusApi(opts: Options): Plugin {
let source: string
let dir: string
let only = 'docs' // UI scans create document collections (meetings come from the batch)
if (rerun) {
const col = body.id ? collectionById(body.id) : undefined
const idx = col ? readIndexFor(col) : null
if (!col || !idx?.root) { sendJson(res, 404, { error: 'collection not found' }); return }
source = idx.root
dir = col.dir
only = (idx as any).only ?? 'all' // preserve the collection's kind on rescan
} else {
source = (body.source ?? '').trim()
if (!source || !isDir(source)) { sendJson(res, 400, { error: `not a directory: ${source}` }); return }
const slug = path.basename(path.resolve(source)).replace(/[^\w.-]+/g, '_') || 'source'
dir = path.join(opts.outputsRoot, slug)
fs.mkdirSync(opts.outputsRoot, { recursive: true })
dir = path.join(opts.outputsRoots[0], slug) // docs land in the first root (doocus-data)
fs.mkdirSync(opts.outputsRoots[0], { recursive: true })
}
try {
await runProcessTree(source, dir)
await runProcessTree(source, dir, only)
sendJson(res, 200, { ok: true, id: path.basename(dir) })
} catch (e) {
sendJson(res, 500, { error: String(e) })
}
}
/** Spawn `uv run python process_tree.py <source> --output <dir>` at repoRoot. */
function runProcessTree(source: string, dir: string): Promise<void> {
/** Spawn `uv run python process_tree.py <source> --output <dir> --only <only>`. */
function runProcessTree(source: string, dir: string, only: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn('uv', ['run', 'python', 'process_tree.py', source, '--output', dir],
const child = spawn('uv',
['run', 'python', 'process_tree.py', source, '--output', dir, '--only', only],
{ cwd: opts.repoRoot })
let err = ''
child.stderr.on('data', (d) => { err += d })
@@ -252,20 +268,42 @@ export function doocusApi(opts: Options): Plugin {
})
}
/** Higher = "richer" version of a file — the one whose sidecar resolves wins,
* so a meetus-data meeting (with .meetus) overrides a doocus copy without it. */
function scoreNode(c: Collection, f: Node): number {
if (f.mode === 'meeting') return fs.existsSync(meetusDir(c, f.path).dir) ? 4 : 2
if (f.mode === 'extracted') {
return f.out && fs.existsSync(path.join(c.dir, f.out, 'content.md')) ? 3 : 1.5
}
return 1
}
function getTree(res: ServerResponse, activeCsv: string | null): void {
const active = activeCsv ? new Set(activeCsv.split(',').filter(Boolean)) : null
const cols = currentCollections().filter((c) => !active || active.has(c.id))
const files: Array<Node & { collection: string }> = []
const counts: Record<string, number> = { extracted: 0, meeting: 0, link: 0, total: 0 }
// Dedup across collections by shared root + rel; the resolving copy wins.
const best = new Map<string, { file: Node & { collection: string; root: string; rel: string }; score: number }>()
for (const c of cols) {
const idx = readIndexFor(c)
if (!idx) continue
for (const f of idx.files) {
files.push({ ...f, collection: c.id, path: `${c.id}/${f.path}` })
counts[f.mode] = (counts[f.mode] ?? 0) + 1
counts.total += 1
const key = `${idx.root}${f.path}`
const score = scoreNode(c, f)
const cur = best.get(key)
if (!cur || score > cur.score) {
best.set(key, {
file: { ...f, collection: c.id, root: idx.root, rel: f.path, path: `${c.id}/${f.path}` },
score,
})
}
}
}
const files = [...best.values()].map((v) => v.file)
const counts: Record<string, number> = { extracted: 0, meeting: 0, link: 0, total: files.length }
for (const f of files) counts[f.mode] = (counts[f.mode] ?? 0) + 1
sendJson(res, 200, {
collections: currentCollections().map((c) => ({ id: c.id, name: c.name })),
files,