this repo has no description
1#!/usr/bin/env node
2import { readdirSync, readFileSync } from 'node:fs';
3import { dirname, join, relative } from 'node:path';
4import { fileURLToPath } from 'node:url';
5
6const DOCS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'docs');
7const EXCLUDED_DIRS = new Set(['archive', 'research']);
8
9function walkMarkdownFiles(dir, base = dir) {
10 const entries = readdirSync(dir, { withFileTypes: true });
11 const files = [];
12
13 for (const entry of entries) {
14 if (entry.name.startsWith('.')) continue;
15 const fullPath = join(dir, entry.name);
16
17 if (entry.isDirectory()) {
18 if (EXCLUDED_DIRS.has(entry.name)) continue;
19 files.push(...walkMarkdownFiles(fullPath, base));
20 } else if (entry.isFile() && entry.name.endsWith('.md')) {
21 files.push(relative(base, fullPath));
22 }
23 }
24
25 return files.sort((a, b) => a.localeCompare(b));
26}
27
28function extractMetadata(fullPath) {
29 const content = readFileSync(fullPath, 'utf8');
30
31 if (!content.startsWith('---')) {
32 return { summary: null, readWhen: [], error: 'missing front matter' };
33 }
34
35 const endIndex = content.indexOf('\n---', 3);
36 if (endIndex === -1) {
37 return { summary: null, readWhen: [], error: 'unterminated front matter' };
38 }
39
40 const frontMatter = content.slice(3, endIndex).trim();
41 const lines = frontMatter.split('\n');
42
43 let summaryLine = null;
44 const readWhen = [];
45 let collectingReadWhen = false;
46
47 for (const rawLine of lines) {
48 const line = rawLine.trim();
49
50 if (line.startsWith('summary:')) {
51 summaryLine = line;
52 collectingReadWhen = false;
53 continue;
54 }
55
56 if (line.startsWith('read_when:')) {
57 collectingReadWhen = true;
58 const inline = line.slice('read_when:'.length).trim();
59 if (inline.startsWith('[') && inline.endsWith(']')) {
60 try {
61 const parsed = JSON.parse(inline.replace(/'/g, '"'));
62 if (Array.isArray(parsed)) {
63 parsed
64 .map((v) => String(v).trim())
65 .filter(Boolean)
66 .forEach((v) => readWhen.push(v));
67 }
68 } catch {
69 /* ignore malformed inline */
70 }
71 }
72 continue;
73 }
74
75 if (collectingReadWhen) {
76 if (line.startsWith('- ')) {
77 const hint = line.slice(2).trim();
78 if (hint) readWhen.push(hint);
79 } else if (line === '') {
80 // allow blank spacer lines inside list
81 } else {
82 collectingReadWhen = false;
83 }
84 }
85 }
86
87 if (!summaryLine) {
88 return { summary: null, readWhen, error: 'summary key missing' };
89 }
90
91 const summaryValue = summaryLine.slice('summary:'.length).trim();
92 const normalized = summaryValue.replace(/^['"]|['"]$/g, '').replace(/\s+/g, ' ').trim();
93
94 if (!normalized) {
95 return { summary: null, readWhen, error: 'summary is empty' };
96 }
97
98 return { summary: normalized, readWhen };
99}
100
101console.log('Listing all markdown files in docs folder:');
102
103const markdownFiles = walkMarkdownFiles(DOCS_DIR);
104
105for (const relativePath of markdownFiles) {
106 const fullPath = join(DOCS_DIR, relativePath);
107 const { summary, readWhen, error } = extractMetadata(fullPath);
108 if (summary) {
109 console.log(`${relativePath} - ${summary}`);
110 if (readWhen.length > 0) {
111 console.log(` Read when: ${readWhen.join('; ')}`);
112 }
113 } else {
114 const reason = error ? ` - [${error}]` : '';
115 console.log(`${relativePath}${reason}`);
116 }
117}
118
119console.log('\nReminder: keep docs up to date as behavior changes. When your task matches any "Read when" hint above (React hooks, cache directives, database work, tests, etc.), read that doc before coding, and suggest new coverage when it is missing.');