oh-my-llmwikimode 1.0.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +494 -0
  3. package/bin/llmwiki.js +1493 -0
  4. package/docs/INSTALLATION.md +228 -0
  5. package/docs/SCOPE_LOCK.md +79 -0
  6. package/docs/STAGE1_GUIDE.md +265 -0
  7. package/docs/STAGE2_AGENT_TEAM_GUIDE.md +141 -0
  8. package/docs/STAGE3_CONVERSATIONAL_GROWTH_GUIDE.md +50 -0
  9. package/docs/TEST_WORKSHEET.md +120 -0
  10. package/docs/github-private-bootstrap.md +53 -0
  11. package/docs/release.md +79 -0
  12. package/docs/stage4-slice1-manual-test.md +259 -0
  13. package/docs/stage4-slice1-user-guide.md +269 -0
  14. package/docs/user-guide-ko.md +452 -0
  15. package/package.json +76 -0
  16. package/scripts/install-llmwiki.ps1 +229 -0
  17. package/src/config.js +74 -0
  18. package/src/curator/browser-data.js +134 -0
  19. package/src/curator/queue.js +324 -0
  20. package/src/curator/schema.js +237 -0
  21. package/src/curator/scoring.js +83 -0
  22. package/src/hooks.js +199 -0
  23. package/src/librarian/schema.js +218 -0
  24. package/src/librarian/weekly-digest.js +478 -0
  25. package/src/security.js +127 -0
  26. package/src/server.js +860 -0
  27. package/src/stage4/graph-reasoning/analyzer.js +255 -0
  28. package/src/stage4/graph-reasoning/browser-data.js +130 -0
  29. package/src/stage4/graph-reasoning/index.js +35 -0
  30. package/src/stage4/graph-reasoning/loader.js +122 -0
  31. package/src/stage4/graph-reasoning/queue.js +154 -0
  32. package/src/stage4/graph-reasoning/schema.js +190 -0
  33. package/src/team/browser-data.js +142 -0
  34. package/src/team/capabilities.js +79 -0
  35. package/src/team/dispatch.js +108 -0
  36. package/src/team/queue.js +290 -0
  37. package/src/team/schema.js +225 -0
  38. package/src/team/shared-memory.js +183 -0
  39. package/src/todo/browser-data.js +71 -0
  40. package/src/todo/queue.js +159 -0
  41. package/src/todo/schema.js +90 -0
  42. package/src/utils/embedding-model.js +111 -0
  43. package/src/wiki/alias-suggestions.js +180 -0
  44. package/src/wiki/browser-data.js +284 -0
  45. package/src/wiki/doctor.js +218 -0
  46. package/src/wiki/entry-normalizer.js +139 -0
  47. package/src/wiki/ingest.js +443 -0
  48. package/src/wiki/lesson-proposal-analyzer.js +463 -0
  49. package/src/wiki/lesson-proposal-manager.js +331 -0
  50. package/src/wiki/lesson-template.js +182 -0
  51. package/src/wiki/lint.js +294 -0
  52. package/src/wiki/notebooklm-adapter.js +264 -0
  53. package/src/wiki/query.js +304 -0
  54. package/src/wiki/raw-manager.js +400 -0
  55. package/src/wiki/search-feedback.js +211 -0
  56. package/src/wiki/semantic-index.js +333 -0
  57. package/src/wiki/semantic-search.js +170 -0
  58. package/src/wiki/source-ledger.js +370 -0
  59. package/src/wiki/store.js +1329 -0
  60. package/src/wiki/usage-events.js +144 -0
@@ -0,0 +1,463 @@
1
+ /**
2
+ * B1 Auto Lesson Proposal — Usage Event Analyzer
3
+ *
4
+ * Analyzes usage event logs to detect repeated patterns
5
+ * and generates lesson proposal artifacts.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import crypto from "node:crypto";
11
+ import { listUsageEventFiles, readUsageEvents, USAGE_EVENTS } from "./usage-events.js";
12
+ import { getWikiPaths, ensureWikiStructure } from "./store.js";
13
+
14
+ export const PROPOSAL_VERSION = 1;
15
+ export const PROPOSAL_DIR = ".system/lesson-proposals";
16
+
17
+ // Minimum thresholds for generating proposals
18
+ const MIN_QUERY_REPEATS = 2;
19
+ const MIN_ENTRY_REPEATS = 2;
20
+ const MIN_SEARCH_RESULT_USED = 3;
21
+ const MAX_ANALYSIS_DAYS = 30;
22
+ const MAX_PROPOSALS = 100;
23
+
24
+ function hashString(input) {
25
+ return crypto.createHash("sha256").update(String(input)).digest("hex").slice(0, 16);
26
+ }
27
+
28
+ function compareStrings(left, right) {
29
+ return String(left ?? "").localeCompare(String(right ?? ""));
30
+ }
31
+
32
+ function ensureProposalDir(wikiRoot) {
33
+ const dir = path.join(wikiRoot, PROPOSAL_DIR);
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ return dir;
36
+ }
37
+
38
+ function dedupKeysForPatternType(type, queryHashes = [], entryPaths = []) {
39
+ if (type === "repeated_query") {
40
+ return [...queryHashes]
41
+ .filter(Boolean)
42
+ .sort(compareStrings)
43
+ .map((queryHash) => "query:" + queryHash);
44
+ }
45
+
46
+ if (type === "repeated_entry") {
47
+ return [...entryPaths]
48
+ .filter(Boolean)
49
+ .sort(compareStrings)
50
+ .map((entryPath) => "entry:" + entryPath);
51
+ }
52
+
53
+ return [];
54
+ }
55
+
56
+ function dedupKeysForProposal(content) {
57
+ const evidence = content?.evidence || {};
58
+ const queryHashes = evidence.query_hashes || content?.query_hashes || [];
59
+ const entryPaths = evidence.entry_paths || [];
60
+ const patternType = content?.pattern_type || content?.type;
61
+ const typedKeys = dedupKeysForPatternType(patternType, queryHashes, entryPaths);
62
+
63
+ if (typedKeys.length > 0) return typedKeys;
64
+
65
+ // Legacy proposals did not store pattern_type. Index all stable keys so
66
+ // already-created proposals remain duplicate-protected after upgrading.
67
+ return [
68
+ ...dedupKeysForPatternType("repeated_query", queryHashes, entryPaths),
69
+ ...dedupKeysForPatternType("repeated_entry", queryHashes, entryPaths),
70
+ ];
71
+ }
72
+
73
+ function dedupKeyForPattern(pattern) {
74
+ const evidence = pattern?.evidence || {};
75
+ const queryHashes = pattern?.query_hash
76
+ ? [pattern.query_hash]
77
+ : (pattern?.query_hashes || evidence.query_hashes || []);
78
+ const entryPaths = pattern?.entry_path
79
+ ? [pattern.entry_path]
80
+ : (evidence.entry_paths || []);
81
+
82
+ return dedupKeysForPatternType(pattern?.type, queryHashes, entryPaths)[0] || "";
83
+ }
84
+
85
+ function listExistingProposals(wikiRoot) {
86
+ const dir = path.join(wikiRoot, PROPOSAL_DIR);
87
+ if (!fs.existsSync(dir)) return new Map();
88
+
89
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
90
+ const existing = new Map();
91
+
92
+ for (const file of files) {
93
+ try {
94
+ const content = JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8"));
95
+ for (const key of dedupKeysForProposal(content)) {
96
+ existing.set(key, content.id);
97
+ }
98
+ } catch {
99
+ // Skip malformed
100
+ }
101
+ }
102
+
103
+ return existing;
104
+ }
105
+
106
+ function countProposals(wikiRoot) {
107
+ const dir = path.join(wikiRoot, PROPOSAL_DIR);
108
+ if (!fs.existsSync(dir)) return 0;
109
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".json")).length;
110
+ }
111
+
112
+ function dateKeyFromUsageFile(fileName) {
113
+ const match = /^(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(fileName);
114
+ return match ? match[1] : "";
115
+ }
116
+
117
+ function deriveUsageEventAnchor(wikiRoot) {
118
+ let latestTimestamp = "";
119
+ let latestEventFileDate = "";
120
+
121
+ for (const fileName of listUsageEventFiles(wikiRoot)) {
122
+ const dateKey = dateKeyFromUsageFile(fileName);
123
+ if (!dateKey) continue;
124
+
125
+ const result = readUsageEvents(wikiRoot, dateKey + "T00:00:00.000Z");
126
+ if (!result.success || !Array.isArray(result.events) || result.events.length === 0) {
127
+ continue;
128
+ }
129
+
130
+ if (compareStrings(dateKey, latestEventFileDate) > 0) {
131
+ latestEventFileDate = dateKey;
132
+ }
133
+
134
+ for (const event of result.events) {
135
+ const timestamp = String(event.ts || "");
136
+ if (
137
+ timestamp
138
+ && !Number.isNaN(new Date(timestamp).getTime())
139
+ && compareStrings(timestamp, latestTimestamp) > 0
140
+ ) {
141
+ latestTimestamp = timestamp;
142
+ }
143
+ }
144
+ }
145
+
146
+ if (latestTimestamp) return new Date(latestTimestamp);
147
+ if (latestEventFileDate) return new Date(latestEventFileDate + "T00:00:00.000Z");
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Read all usage events from the last N days.
153
+ *
154
+ * Deterministic: same anchor + same events = same result.
155
+ * If options.now is omitted, the anchor is derived from the latest usage event.
156
+ * If no events exist, there is no deterministic anchor and the result is empty.
157
+ */
158
+ export function readAllUsageEvents(wikiRoot, options = {}) {
159
+ const days = Math.min(options.days ?? MAX_ANALYSIS_DAYS, MAX_ANALYSIS_DAYS);
160
+ const anchor = options.now ? new Date(options.now) : deriveUsageEventAnchor(wikiRoot);
161
+ if (!anchor) return [];
162
+
163
+ const allEvents = [];
164
+ for (let i = 0; i < days; i++) {
165
+ const date = new Date(anchor);
166
+ date.setUTCDate(date.getUTCDate() - i);
167
+ const result = readUsageEvents(wikiRoot, date);
168
+ if (result.success && Array.isArray(result.events)) {
169
+ allEvents.push(...result.events);
170
+ }
171
+ }
172
+
173
+ // Sort by timestamp for deterministic processing
174
+ allEvents.sort((a, b) => compareStrings(a.ts, b.ts));
175
+
176
+ return allEvents;
177
+ }
178
+
179
+ /**
180
+ * Generate a deterministic proposal ID from stable evidence.
181
+ * Uses evidence content (query hashes + entry paths + timestamps) rather than wall clock
182
+ * so the same usage pattern always produces the same ID.
183
+ */
184
+ export function generateProposalId(evidence) {
185
+ const hashes = [...(evidence.query_hashes || [])].sort(compareStrings).join("|");
186
+ const entries = [...(evidence.entry_paths || [])].sort(compareStrings).join("|");
187
+ const input = `${hashes}::${entries}::${evidence.first_seen || ""}::${evidence.last_seen || ""}`;
188
+ return "lp_" + hashString(input);
189
+ }
190
+
191
+ /**
192
+ * Extract a suggested title from query hashes and entry paths.
193
+ */
194
+ function suggestTitle(evidence) {
195
+ const { query_hashes, entry_paths } = evidence;
196
+
197
+ // Try to extract from entry paths first
198
+ if (entry_paths?.length > 0) {
199
+ const entry = entry_paths[0];
200
+ const basename = path.basename(entry, ".md");
201
+ // Remove date prefix if present
202
+ const clean = basename.replace(/^\d{4}-\d{2}-\d{2}[-T]/, "").replace(/-/g, " ");
203
+ if (clean) return clean.charAt(0).toUpperCase() + clean.slice(1);
204
+ }
205
+
206
+ // Fallback to query hash abbreviation
207
+ if (query_hashes?.length > 0) {
208
+ return `Lesson Proposal ${query_hashes[0].slice(0, 8)}`;
209
+ }
210
+
211
+ return "Lesson Proposal";
212
+ }
213
+
214
+ /**
215
+ * Build a proposal artifact from a detected pattern.
216
+ *
217
+ * Deterministic: same pattern evidence always produces the same artifact
218
+ * (same ID, same created_at, same all fields).
219
+ * created_at defaults to evidence.last_seen so identical usage events
220
+ * always produce identical artifacts regardless of analysis execution time.
221
+ * options.now is accepted only as an explicit override for testing or replay
222
+ * when evidence timestamps are unavailable.
223
+ */
224
+ export function buildProposalArtifact(pattern, index, options = {}) {
225
+ const id = generateProposalId(pattern.evidence);
226
+ // Default to evidence timestamps for determinism.
227
+ // options.now is a last-resort fallback when evidence lacks timestamps.
228
+ const createdAt = pattern.evidence?.last_seen
229
+ || pattern.evidence?.first_seen
230
+ || (options.now ? new Date(options.now).toISOString() : "");
231
+
232
+ return {
233
+ version: PROPOSAL_VERSION,
234
+ id,
235
+ pattern_type: pattern.type || "unknown",
236
+ status: "review_required",
237
+ action: "consider_lesson",
238
+ proposed_title: suggestTitle(pattern.evidence),
239
+ evidence: pattern.evidence,
240
+ template_sections: pattern.template_sections || {
241
+ when_to_use: "_(Auto-generated: fill in context for when this lesson applies)_",
242
+ principles: "_(Auto-generated: fill in key principles)_",
243
+ checklist: ["_(Auto-generated: add checklist items)_"],
244
+ examples: "_(Auto-generated: add examples)_",
245
+ failure_patterns: "_(Auto-generated: add failure patterns)_",
246
+ },
247
+ auto_apply: false,
248
+ review_required: true,
249
+ created_at: createdAt,
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Analyze usage events for repeated patterns.
255
+ */
256
+ export function analyzeUsagePatterns(wikiRoot, options = {}) {
257
+ const events = readAllUsageEvents(wikiRoot, options);
258
+
259
+ if (events.length === 0) {
260
+ return {
261
+ success: true,
262
+ patterns: [],
263
+ total_events_analyzed: 0,
264
+ date_range: null,
265
+ proposals_generated: 0,
266
+ };
267
+ }
268
+
269
+ // Count occurrences
270
+ const queryCounts = new Map();
271
+ const entryCounts = new Map();
272
+ const queryEntries = new Map(); // query_hash -> Set of entry_paths
273
+
274
+ for (const event of events) {
275
+ const queryHash = event.query_hash;
276
+ const entryPath = event.entry_path;
277
+ const eventType = event.event;
278
+
279
+ if (queryHash) {
280
+ queryCounts.set(queryHash, (queryCounts.get(queryHash) || 0) + 1);
281
+ if (!queryEntries.has(queryHash)) queryEntries.set(queryHash, new Set());
282
+ if (entryPath) queryEntries.get(queryHash).add(entryPath);
283
+ }
284
+
285
+ if (entryPath) {
286
+ entryCounts.set(entryPath, (entryCounts.get(entryPath) || 0) + 1);
287
+ }
288
+ }
289
+
290
+ const patterns = [];
291
+ const seenQueryKeys = new Set();
292
+
293
+ // Detect repeated queries
294
+ for (const [queryHash, count] of queryCounts) {
295
+ if (count >= MIN_QUERY_REPEATS) {
296
+ const entries = [...(queryEntries.get(queryHash) || [])].sort(compareStrings);
297
+ const key = queryHash;
298
+
299
+ if (!seenQueryKeys.has(key)) {
300
+ seenQueryKeys.add(key);
301
+ patterns.push({
302
+ type: "repeated_query",
303
+ query_hash: queryHash,
304
+ query_hashes: [queryHash],
305
+ count,
306
+ related_entries: entries,
307
+ confidence: count >= MIN_SEARCH_RESULT_USED ? "high" : "medium",
308
+ evidence: {
309
+ query_hashes: [queryHash],
310
+ entry_paths: entries,
311
+ usage_count: count,
312
+ first_seen: events.find((e) => e.query_hash === queryHash)?.ts || "",
313
+ last_seen: [...events].reverse().find((e) => e.query_hash === queryHash)?.ts || "",
314
+ },
315
+ });
316
+ }
317
+ }
318
+ }
319
+
320
+ // Detect repeated entry references
321
+ for (const [entryPath, count] of entryCounts) {
322
+ if (count >= MIN_ENTRY_REPEATS) {
323
+ const relatedQueries = [...queryCounts.keys()].filter((q) => {
324
+ const entries = queryEntries.get(q);
325
+ return entries?.has(entryPath);
326
+ }).sort(compareStrings);
327
+
328
+ const key = `entry:${entryPath}`;
329
+ if (!seenQueryKeys.has(key)) {
330
+ seenQueryKeys.add(key);
331
+ patterns.push({
332
+ type: "repeated_entry",
333
+ entry_path: entryPath,
334
+ count,
335
+ related_queries: relatedQueries,
336
+ confidence: "medium",
337
+ evidence: {
338
+ query_hashes: relatedQueries,
339
+ entry_paths: [entryPath],
340
+ usage_count: count,
341
+ first_seen: events.find((e) => e.entry_path === entryPath)?.ts || "",
342
+ last_seen: [...events].reverse().find((e) => e.entry_path === entryPath)?.ts || "",
343
+ },
344
+ });
345
+ }
346
+ }
347
+ }
348
+
349
+ // Detect high-usage search_result_used events
350
+ // Instead of creating a duplicate pattern, upgrade repeated_query confidence to "high"
351
+ const searchResultCounts = new Map();
352
+ for (const event of events) {
353
+ if (event.event === USAGE_EVENTS.SEARCH_RESULT_USED && event.query_hash) {
354
+ searchResultCounts.set(event.query_hash, (searchResultCounts.get(event.query_hash) || 0) + 1);
355
+ }
356
+ }
357
+
358
+ for (const [queryHash, count] of searchResultCounts) {
359
+ if (count >= MIN_SEARCH_RESULT_USED) {
360
+ // Find the existing repeated_query pattern and upgrade its confidence
361
+ const existingPattern = patterns.find((p) => p.type === "repeated_query" && p.query_hash === queryHash);
362
+ if (existingPattern) {
363
+ existingPattern.confidence = "high";
364
+ }
365
+ // If there is no repeated_query (e.g. only search_result_used events),
366
+ // create one with high confidence instead of a separate high_usage_search pattern
367
+ if (!existingPattern) {
368
+ const key = queryHash;
369
+ if (!seenQueryKeys.has(key)) {
370
+ seenQueryKeys.add(key);
371
+ const entries = [...(queryEntries.get(queryHash) || [])].sort(compareStrings);
372
+ patterns.push({
373
+ type: "repeated_query",
374
+ query_hash: queryHash,
375
+ query_hashes: [queryHash],
376
+ count,
377
+ related_entries: entries,
378
+ confidence: "high",
379
+ evidence: {
380
+ query_hashes: [queryHash],
381
+ entry_paths: entries,
382
+ usage_count: count,
383
+ first_seen: events.find((e) => e.query_hash === queryHash)?.ts || "",
384
+ last_seen: [...events].reverse().find((e) => e.query_hash === queryHash)?.ts || "",
385
+ },
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ return {
393
+ success: true,
394
+ patterns,
395
+ total_events_analyzed: events.length,
396
+ date_range: {
397
+ from: events[0]?.ts || "",
398
+ to: events[events.length - 1]?.ts || "",
399
+ },
400
+ };
401
+ }
402
+
403
+ /**
404
+ * Generate and save lesson proposals from usage patterns.
405
+ */
406
+ export function generateLessonProposals(wikiRoot, options = {}) {
407
+ try {
408
+ const paths = getWikiPaths(wikiRoot);
409
+ ensureWikiStructure(paths);
410
+ ensureProposalDir(wikiRoot);
411
+
412
+ const analysis = analyzeUsagePatterns(wikiRoot, options);
413
+ if (!analysis.success) return analysis;
414
+
415
+ const existingProposals = listExistingProposals(wikiRoot);
416
+ const currentCount = countProposals(wikiRoot);
417
+ let generated = 0;
418
+ let skipped = 0;
419
+
420
+ // Track in-memory IDs within this run to prevent same-run collisions
421
+ const generatedThisRun = new Set();
422
+
423
+ for (let i = 0; i < analysis.patterns.length; i++) {
424
+ if (currentCount + generated >= MAX_PROPOSALS) break;
425
+
426
+ const pattern = analysis.patterns[i];
427
+ const dedupKey = dedupKeyForPattern(pattern);
428
+ if (!dedupKey) {
429
+ skipped++;
430
+ continue;
431
+ }
432
+
433
+ // Skip if already proposed (on disk or in this run)
434
+ if (existingProposals.has(dedupKey) || generatedThisRun.has(dedupKey)) {
435
+ skipped++;
436
+ continue;
437
+ }
438
+
439
+ const proposal = buildProposalArtifact(pattern, i, options);
440
+ const filePath = path.join(wikiRoot, PROPOSAL_DIR, `${proposal.id}.json`);
441
+
442
+ // NEVER overwrite an existing proposal file (preserves applied/rejected status)
443
+ if (fs.existsSync(filePath)) {
444
+ skipped++;
445
+ continue;
446
+ }
447
+
448
+ fs.writeFileSync(filePath, JSON.stringify(proposal, null, 2), "utf-8");
449
+ generated++;
450
+ generatedThisRun.add(dedupKey);
451
+ }
452
+
453
+ return {
454
+ success: true,
455
+ proposals_generated: generated,
456
+ proposals_skipped: skipped,
457
+ total_patterns: analysis.patterns.length,
458
+ total_events: analysis.total_events_analyzed,
459
+ };
460
+ } catch (error) {
461
+ return { success: false, error: error.message };
462
+ }
463
+ }