openclew 0.2.1 → 0.4.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.
@@ -1,226 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- openclew index generator.
4
-
5
- Scans doc/_*.md (refdocs) and doc/log/*.md (logs),
6
- parses metadata line + L1 blocks, and generates doc/_INDEX.md.
7
-
8
- Usage:
9
- python generate-index.py # from project root
10
- python generate-index.py /path/to/doc # custom doc directory
11
-
12
- Idempotent: running twice produces the same output.
13
- Zero dependencies: Python 3.8+ standard library only.
14
- """
15
-
16
- import os
17
- import re
18
- import sys
19
- from datetime import datetime
20
- from pathlib import Path
21
-
22
-
23
- def find_doc_dir():
24
- """Find the doc/ directory."""
25
- if len(sys.argv) > 1:
26
- doc_dir = Path(sys.argv[1])
27
- else:
28
- doc_dir = Path("doc")
29
-
30
- if not doc_dir.is_dir():
31
- print(f"No '{doc_dir}' directory found. Nothing to index.")
32
- sys.exit(0)
33
-
34
- return doc_dir
35
-
36
-
37
- def parse_metadata_line(content):
38
- """Extract metadata from the first line (before L1_START).
39
-
40
- Format: openclew@VERSION · key: value · key: value · ...
41
- """
42
- meta = {}
43
- first_line = content.split("\n", 1)[0].strip()
44
- if not first_line.startswith("openclew@"):
45
- return meta
46
-
47
- parts = first_line.split(" · ")
48
- for part in parts:
49
- part = part.strip()
50
- if part.startswith("openclew@"):
51
- meta["version"] = part.split("@", 1)[1]
52
- continue
53
- if ":" in part:
54
- key, _, value = part.partition(":")
55
- meta[key.strip().lower()] = value.strip()
56
-
57
- return meta
58
-
59
-
60
- def parse_l1(content):
61
- """Extract L1 fields (subject, doc_brief) from L1_START/L1_END block."""
62
- meta = {}
63
- match = re.search(
64
- r"<!--\s*L1_START\s*-->(.+?)<!--\s*L1_END\s*-->",
65
- content,
66
- re.DOTALL,
67
- )
68
- if not match:
69
- return meta
70
-
71
- block = match.group(1)
72
-
73
- # Extract **subject:** value
74
- subject_match = re.search(r"\*\*subject:\*\*\s*(.+)", block)
75
- if subject_match:
76
- meta["subject"] = subject_match.group(1).strip()
77
-
78
- # Extract **doc_brief:** value
79
- brief_match = re.search(r"\*\*doc_brief:\*\*\s*(.+)", block)
80
- if brief_match:
81
- meta["doc_brief"] = brief_match.group(1).strip()
82
-
83
- return meta
84
-
85
-
86
- def parse_l1_legacy(content):
87
- """Fallback parser for old format (key: value lines inside L1 block)."""
88
- meta = {}
89
- match = re.search(
90
- r"<!--\s*L1_START\s*-->(.+?)<!--\s*L1_END\s*-->",
91
- content,
92
- re.DOTALL,
93
- )
94
- if not match:
95
- return meta
96
-
97
- block = match.group(1)
98
- for line in block.splitlines():
99
- line = line.strip()
100
- if line.startswith("#") or not line:
101
- continue
102
- if ":" in line:
103
- key, _, value = line.partition(":")
104
- meta[key.strip().lower()] = value.strip()
105
-
106
- return meta
107
-
108
-
109
- def parse_file(filepath):
110
- """Parse a file and return combined metadata + L1 fields."""
111
- try:
112
- content = filepath.read_text(encoding="utf-8")
113
- except (OSError, UnicodeDecodeError):
114
- return None
115
-
116
- # Try new format first
117
- meta_line = parse_metadata_line(content)
118
- l1 = parse_l1(content)
119
-
120
- if l1.get("subject"):
121
- # New format
122
- meta_line.update(l1)
123
- return meta_line
124
-
125
- # Fallback to legacy format
126
- legacy = parse_l1_legacy(content)
127
- if legacy:
128
- meta_line.update(legacy)
129
- return meta_line
130
-
131
- return None
132
-
133
-
134
- def collect_docs(doc_dir):
135
- """Collect refdocs and logs with their metadata."""
136
- refdocs = []
137
- logs = []
138
-
139
- # Refdocs: doc/_*.md
140
- for f in sorted(doc_dir.glob("_*.md")):
141
- if f.name == "_INDEX.md":
142
- continue
143
- meta = parse_file(f)
144
- if meta:
145
- refdocs.append((f, meta))
146
-
147
- # Log docs: doc/log/*.md
148
- log_dir = doc_dir / "log"
149
- if log_dir.is_dir():
150
- for f in sorted(log_dir.glob("*.md"), reverse=True):
151
- meta = parse_file(f)
152
- if meta:
153
- logs.append((f, meta))
154
-
155
- return refdocs, logs
156
-
157
-
158
- def generate_index(doc_dir, refdocs, logs):
159
- """Generate _INDEX.md content."""
160
- now = datetime.now().strftime("%Y-%m-%d %H:%M")
161
- lines = [
162
- f"# Project Knowledge Index",
163
- f"",
164
- f"> Auto-generated by [openclew](https://github.com/openclew/openclew) on {now}.",
165
- f"> Do not edit manually — rebuilt from L1 metadata on every commit.",
166
- f"",
167
- ]
168
-
169
- # Refdocs section
170
- lines.append("## Refdocs")
171
- lines.append("")
172
- if refdocs:
173
- lines.append("| Document | Subject | Status | Category |")
174
- lines.append("|----------|---------|--------|----------|")
175
- for f, meta in refdocs:
176
- name = f.name
177
- subject = meta.get("subject", "—")
178
- status = meta.get("status", "—")
179
- category = meta.get("category", "—")
180
- rel_path = f.relative_to(doc_dir.parent)
181
- lines.append(f"| [{name}]({rel_path}) | {subject} | {status} | {category} |")
182
- else:
183
- lines.append("_No refdocs yet. Create one with `templates/refdoc.md`._")
184
- lines.append("")
185
-
186
- # Logs section (last 20)
187
- lines.append("## Recent logs")
188
- lines.append("")
189
- display_logs = logs[:20]
190
- if display_logs:
191
- lines.append("| Date | Subject | Status | Category |")
192
- lines.append("|------|---------|--------|----------|")
193
- for f, meta in display_logs:
194
- date = meta.get("date", f.stem[:10])
195
- subject = meta.get("subject", "—")
196
- status = meta.get("status", "—")
197
- category = meta.get("category", "—")
198
- rel_path = f.relative_to(doc_dir.parent)
199
- lines.append(f"| {date} | [{subject}]({rel_path}) | {status} | {category} |")
200
- if len(logs) > 20:
201
- lines.append(f"")
202
- lines.append(f"_{len(logs) - 20} older logs not shown._")
203
- else:
204
- lines.append("_No logs yet. Create one with `templates/log.md`._")
205
- lines.append("")
206
-
207
- # Stats
208
- lines.append("---")
209
- lines.append(f"**{len(refdocs)}** refdocs, **{len(logs)}** logs.")
210
- lines.append("")
211
-
212
- return "\n".join(lines)
213
-
214
-
215
- def main():
216
- doc_dir = find_doc_dir()
217
- refdocs, logs = collect_docs(doc_dir)
218
- index_content = generate_index(doc_dir, refdocs, logs)
219
-
220
- index_path = doc_dir / "_INDEX.md"
221
- index_path.write_text(index_content, encoding="utf-8")
222
- print(f"Generated {index_path} ({len(refdocs)} refdocs, {len(logs)} logs)")
223
-
224
-
225
- if __name__ == "__main__":
226
- main()