openclew 0.2.0 → 0.3.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.
@@ -0,0 +1,299 @@
1
+ # openclew document format
2
+
3
+ > SSOT — this file defines the canonical format for all openclew documents.
4
+ > Templates (`refdoc.md`, `log.md`), embedded templates (`lib/templates.js`), and parsers (`generate-index.py`, `lib/search.js`) must conform to this spec.
5
+
6
+ ---
7
+
8
+ ## File structure
9
+
10
+ Every openclew document is a single Markdown file with 4 sections:
11
+
12
+ ```
13
+ ┌─────────────────────────────────────────────────┐
14
+ │ Line 1 — Metadata line │
15
+ │ Condensed key-value pairs for indexing │
16
+ ├─────────────────────────────────────────────────┤
17
+ │ L1 — Subject + Brief │
18
+ │ ~40 tokens. "Should I read this?" │
19
+ ├─────────────────────────────────────────────────┤
20
+ │ L2 — Summary │
21
+ │ Essential context. Enough for most decisions. │
22
+ ├─────────────────────────────────────────────────┤
23
+ │ L3 — Details │
24
+ │ Full technical content. Deep-dive only. │
25
+ └─────────────────────────────────────────────────┘
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Line 1 — Metadata
31
+
32
+ A single line of condensed key-value pairs separated by ` · `. Always the first line of the file, no blank line before it.
33
+
34
+ ### Refdoc format
35
+
36
+ ```
37
+ openclew@VERSION · created: YYYY-MM-DD · updated: YYYY-MM-DD · type: TYPE · status: STATUS · category: CATEGORY · keywords: [tag1, tag2]
38
+ ```
39
+
40
+ ### Log format
41
+
42
+ ```
43
+ openclew@VERSION · date: YYYY-MM-DD · type: TYPE · status: STATUS · category: CATEGORY · keywords: [tag1, tag2]
44
+ ```
45
+
46
+ ### Fields
47
+
48
+ | Field | Refdoc | Log | Description |
49
+ |-------|:------:|:---:|-------------|
50
+ | `openclew@VERSION` | ✅ | ✅ | Package version that created the doc |
51
+ | `created` | ✅ | — | Creation date |
52
+ | `updated` | ✅ | — | Last update date |
53
+ | `date` | — | ✅ | Session date |
54
+ | `type` | ✅ | ✅ | Document type (see below) |
55
+ | `status` | ✅ | ✅ | Document status (see below) |
56
+ | `category` | ✅ | ✅ | Main domain (free text) |
57
+ | `keywords` | ✅ | ✅ | Tags for search `[tag1, tag2]` |
58
+
59
+ ### Types
60
+
61
+ | Type | Usage |
62
+ |------|-------|
63
+ | `Reference` | Durable knowledge (architecture, conventions, decisions) |
64
+ | `Architecture` | Structural design document |
65
+ | `Guide` | How-to, onboarding, process explanation |
66
+ | `Analysis` | Investigation, comparison, study |
67
+ | `Bug` | Bug investigation and fix |
68
+ | `Feature` | New functionality |
69
+ | `Refactor` | Code restructuring |
70
+ | `Doc` | Documentation-only change |
71
+ | `Deploy` | Deployment or release |
72
+
73
+ ### Statuses
74
+
75
+ | Status | Refdoc | Log | Description |
76
+ |--------|:------:|:---:|-------------|
77
+ | `Active` | ✅ | — | Living document, actively maintained |
78
+ | `Stable` | ✅ | — | Mature, rarely updated |
79
+ | `Archived` | ✅ | — | No longer relevant, kept for history |
80
+ | `In progress` | — | ✅ | Work ongoing |
81
+ | `Done` | — | ✅ | Work completed |
82
+ | `Abandoned` | — | ✅ | Work stopped, approach not viable |
83
+
84
+ ---
85
+
86
+ ## L1 — Subject + Brief
87
+
88
+ Between `<!-- L1_START -->` and `<!-- L1_END -->` markers. Two fields only:
89
+
90
+ ```markdown
91
+ <!-- L1_START -->
92
+ **subject:** Short title (< 60 chars)
93
+
94
+ **doc_brief:** 1-2 sentences of **assertions** (what is true), not narration (what was done). Must be enough to decide if you need to read further.
95
+ <!-- L1_END -->
96
+ ```
97
+
98
+ ### Rules
99
+
100
+ - **subject** is the document's title. Keep it short and scannable.
101
+ - **doc_brief** answers: "Should I read this?" Write **assertions** — state what is true, decided, or concluded. Don't narrate what was done. Quick test: remove "we investigated" / "we compared" — if the sentence collapses, rewrite it.
102
+ - Bad: "Investigated memory leak in worker pool. Profiled with pprof, tested several fixes."
103
+ - Good: "Worker pool leaked memory via unclosed response bodies in retry path. Fixed with deferred close. Steady at 120MB under load."
104
+ - Both fields use `**bold_key:**` syntax (not YAML `key: value`).
105
+ - No other fields in L1. All metadata lives on line 1.
106
+
107
+ ---
108
+
109
+ ## L2 — Summary
110
+
111
+ Between `<!-- L2_START -->` and `<!-- L2_END -->` markers. Starts with `# L2 - Summary`.
112
+
113
+ ```markdown
114
+ <!-- L2_START -->
115
+ # L2 - Summary
116
+
117
+ ## Objective
118
+ <!-- Why this document exists / why this work was done -->
119
+
120
+ ## Key points
121
+ <!-- 3-5 essential takeaways -->
122
+
123
+ ## Solution
124
+ <!-- Recommended approach or what was implemented -->
125
+
126
+ ## Watch out
127
+ <!-- Pitfalls, edge cases, limitations -->
128
+ <!-- L2_END -->
129
+ ```
130
+
131
+ ### Rules
132
+
133
+ - Must fit on one screen (~40 lines of ~80 chars).
134
+ - No emojis in headers.
135
+ - Sections are suggested, not mandatory — adapt to the content.
136
+ - Refdocs typically use: Objective, Key points, Solution, Watch out.
137
+ - Logs typically use: Objective, Problem, Solution, Watch out.
138
+
139
+ ---
140
+
141
+ ## L3 — Details
142
+
143
+ Between `<!-- L3_START -->` and `<!-- L3_END -->` markers. Starts with `# L3 - Details`.
144
+
145
+ ```markdown
146
+ <!-- L3_START -->
147
+ # L3 - Details
148
+
149
+ <!-- Full technical content: code examples, diagrams, deep dives... -->
150
+
151
+ ---
152
+
153
+ ## Changelog
154
+
155
+ | Date | Change |
156
+ |------|--------|
157
+ | YYYY-MM-DD | Initial creation |
158
+ <!-- L3_END -->
159
+ ```
160
+
161
+ ### Rules
162
+
163
+ - Exhaustive — include everything that might be needed later.
164
+ - Code examples, before/after, commands, links.
165
+ - Changelog table at the end (refdocs only — logs are immutable).
166
+
167
+ ---
168
+
169
+ ## Naming conventions
170
+
171
+ | Type | Location | Naming |
172
+ |------|----------|--------|
173
+ | Refdoc | `doc/_SUBJECT.md` | UPPER_SNAKE_CASE, prefixed `_` |
174
+ | Log | `doc/log/YYYY-MM-DD_subject.md` | lowercase-with-hyphens, dated |
175
+ | Index | `doc/_INDEX.md` | Auto-generated, never edit manually |
176
+
177
+ ---
178
+
179
+ ## Parser compatibility
180
+
181
+ The `generate-index.py` parser extracts:
182
+ 1. **Line 1 metadata**: splits on ` · `, parses `key: value` pairs
183
+ 2. **L1 fields**: regex for `**subject:**` and `**doc_brief:**` between L1 markers
184
+
185
+ **Legacy fallback**: if no `**subject:**` is found in L1, the parser falls back to plain `key: value` lines (for docs created before the `**bold:**` convention).
186
+
187
+ **Line 1 prefix**: must start with `openclew@`. Files starting with other prefixes (e.g. `R.AlphA.Doc@`) are not parsed by the openclew toolchain but remain valid Markdown.
188
+
189
+ ---
190
+
191
+ ## Complete examples
192
+
193
+ ### Refdoc example
194
+
195
+ ```markdown
196
+ openclew@0.2.1 · created: 2026-03-07 · updated: 2026-03-20 · type: Reference · status: Active · category: Auth · keywords: [JWT, sessions, Redis]
197
+
198
+ <!-- L1_START -->
199
+ **subject:** Authentication architecture
200
+
201
+ **doc_brief:** JWT-based auth with refresh tokens. Sessions stored in Redis with 15-min expiry. Google OAuth as sole provider. Token refresh handled client-side.
202
+ <!-- L1_END -->
203
+
204
+ ---
205
+
206
+ <!-- L2_START -->
207
+ # L2 - Summary
208
+
209
+ ## Objective
210
+ Document the auth architecture so agents and new devs can work on auth-related code without re-investigating.
211
+
212
+ ## Key points
213
+ - JWT access tokens (15 min) + refresh tokens (7 days)
214
+ - Redis for session storage (not Postgres — latency matters)
215
+ - Google OAuth only (no email/password)
216
+ - Refresh handled client-side via interceptor
217
+
218
+ ## Watch out
219
+ - Refresh endpoint has no rate limiting yet — track issue #42
220
+ - Test tokens in .env.test, never commit real secrets
221
+ <!-- L2_END -->
222
+
223
+ ---
224
+
225
+ <!-- L3_START -->
226
+ # L3 - Details
227
+
228
+ ## Token flow
229
+ Client → /auth/google → JWT + refresh token
230
+ Client → /auth/refresh → new JWT (if refresh valid)
231
+
232
+ ## Key files
233
+ - src/routes/auth.ts — OAuth + refresh endpoints
234
+ - src/middleware/auth.ts — JWT verification
235
+ - src/services/session.ts — Redis session management
236
+
237
+ ---
238
+
239
+ ## Changelog
240
+
241
+ | Date | Change |
242
+ |------|--------|
243
+ | 2026-03-20 | Added rate limiting note |
244
+ | 2026-03-07 | Initial creation |
245
+ <!-- L3_END -->
246
+ ```
247
+
248
+ ### Log example
249
+
250
+ ```markdown
251
+ openclew@0.2.1 · date: 2026-03-15 · type: Bug · status: Done · category: Auth · keywords: [token, refresh, race condition]
252
+
253
+ <!-- L1_START -->
254
+ **subject:** Fix token refresh race condition
255
+
256
+ **doc_brief:** Two concurrent requests could trigger double refresh, invalidating both tokens. Fixed with a mutex in the interceptor. No more 401 cascades.
257
+ <!-- L1_END -->
258
+
259
+ ---
260
+
261
+ <!-- L2_START -->
262
+ # L2 - Summary
263
+
264
+ ## Objective
265
+ Fix intermittent 401 errors when multiple API calls happen during token refresh.
266
+
267
+ ## Problem
268
+ Two concurrent requests both detect an expired token and both call /auth/refresh. The second call invalidates the first's new token, causing a 401 cascade.
269
+
270
+ ## Solution
271
+ Added a refresh mutex in the HTTP interceptor. First request triggers refresh, others wait for the result. Single refresh call per expiry cycle.
272
+ <!-- L2_END -->
273
+
274
+ ---
275
+
276
+ <!-- L3_START -->
277
+ # L3 - Details
278
+
279
+ ## Root cause
280
+ The interceptor checked `isTokenExpired()` synchronously but `refreshToken()` was async. Between the check and the refresh response, other requests would also see the expired token and trigger their own refresh.
281
+
282
+ ## Fix (src/lib/http-client.ts)
283
+ ```ts
284
+ let refreshPromise = null;
285
+
286
+ async function ensureValidToken() {
287
+ if (!isTokenExpired()) return;
288
+ if (!refreshPromise) {
289
+ refreshPromise = refreshToken().finally(() => { refreshPromise = null; });
290
+ }
291
+ await refreshPromise;
292
+ }
293
+ ```
294
+
295
+ ## Testing
296
+ - Added test: 10 concurrent requests during token expiry → exactly 1 refresh call
297
+ - Manual test: rapid navigation between pages during expiry window
298
+ <!-- L3_END -->
299
+ ```
package/templates/log.md CHANGED
@@ -1,12 +1,9 @@
1
+ openclew@VERSION · date: YYYY-MM-DD · type: Bug | Feature | Refactor | Doc | Deploy · status: Done | In progress | Abandoned · category: Main domain · keywords: [tag1, tag2, tag3]
2
+
1
3
  <!-- L1_START -->
2
- # L1 - Metadata
3
- date: YYYY-MM-DD
4
- type: Bug | Feature | Refactor | Doc | Deploy
5
- subject: Short title (< 60 chars)
6
- short_story: 1-2 sentences. What happened and what was the outcome. Include conclusions, not just process.
7
- status: Done | In progress | Abandoned
8
- category: Main domain (e.g. Auth, API, Database, UI...)
9
- keywords: [tag1, tag2, tag3]
4
+ **subject:** Short title (< 60 chars)
5
+
6
+ **doc_brief:** 1-2 sentences of assertions (what is true/decided), not narration (what was done). Remove "we did X" — state the result.
10
7
  <!-- L1_END -->
11
8
 
12
9
  ---
@@ -0,0 +1,59 @@
1
+ # Onboarding openclew — Flow
2
+
3
+ ## Principe
4
+
5
+ L'onboarding openclew guide un utilisateur pour structurer la documentation de son projet.
6
+ Le résultat minimum = un `doc/_INDEX.md` (point d'entrée) lisible par les agents IA.
7
+
8
+ ## Détection
9
+
10
+ | Signal | Résultat |
11
+ |--------|----------|
12
+ | `doc/_INDEX.md` existe | Projet configuré → pas d'onboarding |
13
+ | `.openclew.json` existe | Projet configuré → pas d'onboarding |
14
+ | `doc/_*.md` existe (sans _INDEX) | Projet partiel → proposer complétion |
15
+ | Rien | Projet vierge → proposer setup complet |
16
+
17
+ ## Étapes
18
+
19
+ ### 1. Détection automatique (client)
20
+
21
+ Au lancement, le client IDE scanne le workspace :
22
+ - Cherche `doc/_INDEX.md` ou `.openclew.json`
23
+ - Si absent → affiche un CTA "Configurer la documentation projet"
24
+ - Si présent → flow normal (enrichissement L1/L2)
25
+
26
+ ### 2. Scaffold (bot-guided)
27
+
28
+ Quand l'utilisateur accepte, le bot :
29
+ 1. Crée `doc/` si absent
30
+ 2. Scanne les fichiers existants (README, code, config) pour comprendre le projet
31
+ 3. Génère `doc/_INDEX.md` à partir du template `scaffold_index.md`
32
+ 4. Propose un premier refdoc (`doc/_ARCHITECTURE.md` ou similaire) basé sur ce qu'il a détecté
33
+
34
+ ### 3. Validation
35
+
36
+ Le bot montre le résultat et demande confirmation. L'utilisateur peut :
37
+ - Accepter tel quel
38
+ - Demander des modifications
39
+ - Ajouter d'autres refdocs
40
+
41
+ ### 4. Complétion
42
+
43
+ L'onboarding est marqué `completed` pour ce workspace. Le knowledge provider prend le relais normalement.
44
+
45
+ ## Déclenchement
46
+
47
+ | Méthode | Quand |
48
+ |---------|-------|
49
+ | Automatique | Premier message dans un projet sans openclew |
50
+ | Manuel | Slash command `/setup` |
51
+ | Re-déclenchement | `/setup` fonctionne même si déjà complété (pour ajouter des docs) |
52
+
53
+ ## Prompt bot
54
+
55
+ Le bot reçoit un contexte enrichi qui l'instruit de :
56
+ 1. Lister les fichiers du projet (via `list_files`)
57
+ 2. Identifier le langage principal, le framework, la structure
58
+ 3. Créer `doc/_INDEX.md` avec les métadonnées détectées
59
+ 4. Proposer 1-3 refdocs pertinents selon le type de projet
@@ -0,0 +1,31 @@
1
+ openclew@1.0.0 · created: {{DATE}} · updated: {{DATE}} · type: Reference · status: Active · category: Index · keywords: [index, documentation, entry-point]
2
+
3
+ <!-- L1_START -->
4
+ **subject:** {{PROJECT_NAME}} — Documentation index
5
+
6
+ **doc_brief:** Point d'entrée documentation du projet {{PROJECT_NAME}}. Liste les refdocs disponibles avec leur sujet et statut.
7
+ <!-- L1_END -->
8
+
9
+ ---
10
+
11
+ <!-- L2_START -->
12
+ # L2 - Summary
13
+
14
+ ## Project
15
+
16
+ - **Name**: {{PROJECT_NAME}}
17
+ - **Stack**: {{STACK}}
18
+ - **Description**: {{DESCRIPTION}}
19
+
20
+ ## Refdocs
21
+
22
+ | Document | Subject | Status |
23
+ |----------|---------|--------|
24
+ {{REFDOCS_TABLE}}
25
+
26
+ ## Logs
27
+
28
+ Session logs in `doc/log/`. Most recent:
29
+
30
+ {{RECENT_LOGS}}
31
+ <!-- L2_END -->
@@ -1,13 +1,9 @@
1
+ openclew@VERSION · created: YYYY-MM-DD · updated: YYYY-MM-DD · type: Reference | Architecture | Guide | Analysis · status: Active | Stable | Archived · category: Main domain · keywords: [tag1, tag2, tag3]
2
+
1
3
  <!-- L1_START -->
2
- # L1 - Metadata
3
- type: Reference | Architecture | Guide | Analysis
4
- subject: Short title (< 60 chars)
5
- created: YYYY-MM-DD
6
- updated: YYYY-MM-DD
7
- short_story: 1-2 sentences. What this doc covers and what it concludes. Must be enough to decide if you need to read further.
8
- status: Active | Stable | Archived
9
- category: Main domain (e.g. Auth, API, Database, UI...)
10
- keywords: [tag1, tag2, tag3]
4
+ **subject:** Short title (< 60 chars)
5
+
6
+ **doc_brief:** 1-2 sentences of assertions (what is true/decided), not narration (what was done). Must be enough to decide if you need to read further.
11
7
  <!-- L1_END -->
12
8
 
13
9
  ---
@@ -1,157 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- openclew index generator.
4
-
5
- Scans doc/_*.md (living docs) and doc/log/*.md (logs),
6
- parses L1 metadata 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_l1(filepath):
38
- """Extract L1 metadata from a file."""
39
- try:
40
- content = filepath.read_text(encoding="utf-8")
41
- except (OSError, UnicodeDecodeError):
42
- return None
43
-
44
- match = re.search(
45
- r"<!--\s*L1_START\s*-->(.+?)<!--\s*L1_END\s*-->",
46
- content,
47
- re.DOTALL,
48
- )
49
- if not match:
50
- return None
51
-
52
- block = match.group(1)
53
- meta = {}
54
- for line in block.splitlines():
55
- line = line.strip()
56
- if line.startswith("#") or not line:
57
- continue
58
- if ":" in line:
59
- key, _, value = line.partition(":")
60
- meta[key.strip().lower()] = value.strip()
61
-
62
- return meta
63
-
64
-
65
- def collect_docs(doc_dir):
66
- """Collect living docs and logs with their L1 metadata."""
67
- living_docs = []
68
- logs = []
69
-
70
- # Living docs: doc/_*.md
71
- for f in sorted(doc_dir.glob("_*.md")):
72
- if f.name == "_INDEX.md":
73
- continue
74
- meta = parse_l1(f)
75
- if meta:
76
- living_docs.append((f, meta))
77
-
78
- # Log docs: doc/log/*.md
79
- log_dir = doc_dir / "log"
80
- if log_dir.is_dir():
81
- for f in sorted(log_dir.glob("*.md"), reverse=True):
82
- meta = parse_l1(f)
83
- if meta:
84
- logs.append((f, meta))
85
-
86
- return living_docs, logs
87
-
88
-
89
- def generate_index(doc_dir, living_docs, logs):
90
- """Generate _INDEX.md content."""
91
- now = datetime.now().strftime("%Y-%m-%d %H:%M")
92
- lines = [
93
- f"# Project Knowledge Index",
94
- f"",
95
- f"> Auto-generated by [openclew](https://github.com/openclew/openclew) on {now}.",
96
- f"> Do not edit manually — rebuilt from L1 metadata on every commit.",
97
- f"",
98
- ]
99
-
100
- # Living docs section
101
- lines.append("## Living docs")
102
- lines.append("")
103
- if living_docs:
104
- lines.append("| Document | Subject | Status | Category |")
105
- lines.append("|----------|---------|--------|----------|")
106
- for f, meta in living_docs:
107
- name = f.name
108
- subject = meta.get("subject", "—")
109
- status = meta.get("status", "—")
110
- category = meta.get("category", "—")
111
- rel_path = f.relative_to(doc_dir.parent)
112
- lines.append(f"| [{name}]({rel_path}) | {subject} | {status} | {category} |")
113
- else:
114
- lines.append("_No living docs yet. Create one with `templates/living.md`._")
115
- lines.append("")
116
-
117
- # Logs section (last 20)
118
- lines.append("## Recent logs")
119
- lines.append("")
120
- display_logs = logs[:20]
121
- if display_logs:
122
- lines.append("| Date | Subject | Status | Category |")
123
- lines.append("|------|---------|--------|----------|")
124
- for f, meta in display_logs:
125
- date = meta.get("date", f.stem[:10])
126
- subject = meta.get("subject", "—")
127
- status = meta.get("status", "—")
128
- category = meta.get("category", "—")
129
- rel_path = f.relative_to(doc_dir.parent)
130
- lines.append(f"| {date} | [{subject}]({rel_path}) | {status} | {category} |")
131
- if len(logs) > 20:
132
- lines.append(f"")
133
- lines.append(f"_{len(logs) - 20} older logs not shown._")
134
- else:
135
- lines.append("_No logs yet. Create one with `templates/log.md`._")
136
- lines.append("")
137
-
138
- # Stats
139
- lines.append("---")
140
- lines.append(f"**{len(living_docs)}** living docs, **{len(logs)}** logs.")
141
- lines.append("")
142
-
143
- return "\n".join(lines)
144
-
145
-
146
- def main():
147
- doc_dir = find_doc_dir()
148
- living_docs, logs = collect_docs(doc_dir)
149
- index_content = generate_index(doc_dir, living_docs, logs)
150
-
151
- index_path = doc_dir / "_INDEX.md"
152
- index_path.write_text(index_content, encoding="utf-8")
153
- print(f"Generated {index_path} ({len(living_docs)} living docs, {len(logs)} logs)")
154
-
155
-
156
- if __name__ == "__main__":
157
- main()