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,255 @@
1
+ import { validateGraphInsightArtifact } from "./schema.js";
2
+
3
+ function compareStrings(left, right) {
4
+ return String(left ?? "").localeCompare(String(right ?? ""));
5
+ }
6
+
7
+ export function calculateNodeDegrees(graph) {
8
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
9
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
10
+ const degrees = new Map();
11
+
12
+ for (const node of nodes) {
13
+ const nodeId = node.id || node.path || "";
14
+ if (nodeId) degrees.set(nodeId, 0);
15
+ }
16
+
17
+ for (const edge of edges) {
18
+ const source = edge.source || edge.from || "";
19
+ const target = edge.target || edge.to || "";
20
+ if (degrees.has(source)) degrees.set(source, degrees.get(source) + 1);
21
+ if (degrees.has(target)) degrees.set(target, degrees.get(target) + 1);
22
+ }
23
+
24
+ return degrees;
25
+ }
26
+
27
+ export function deriveDeterministicReferenceTime(inputs) {
28
+ const dates = [];
29
+
30
+ const graphMeta = inputs.graph?.meta || {};
31
+ if (typeof graphMeta.built_at === "string" && graphMeta.built_at.trim()) {
32
+ dates.push(graphMeta.built_at);
33
+ }
34
+
35
+ for (const card of inputs.curatorQueue?.cards || []) {
36
+ if (typeof card.created_at === "string" && card.created_at.trim()) {
37
+ dates.push(card.created_at);
38
+ }
39
+ }
40
+
41
+ for (const record of inputs.curatorAudit || []) {
42
+ if (typeof record.timestamp === "string" && record.timestamp.trim()) {
43
+ dates.push(record.timestamp);
44
+ }
45
+ }
46
+
47
+ const browserMeta = inputs.browserData?.meta || {};
48
+ if (typeof browserMeta.built_at === "string" && browserMeta.built_at.trim()) {
49
+ dates.push(browserMeta.built_at);
50
+ }
51
+
52
+ if (dates.length === 0) return "1970-01-01T00:00:00.000Z";
53
+ return dates.sort((a, b) => a.localeCompare(b)).at(-1);
54
+ }
55
+
56
+ function daysBetween(referenceTime, candidateTime) {
57
+ try {
58
+ const ref = Date.parse(referenceTime);
59
+ const cand = Date.parse(candidateTime);
60
+ if (Number.isNaN(ref) || Number.isNaN(cand)) return 0;
61
+ return Math.max(0, Math.floor((ref - cand) / (1000 * 60 * 60 * 24)));
62
+ } catch {
63
+ return 0;
64
+ }
65
+ }
66
+
67
+ function median(values) {
68
+ if (values.length === 0) return 0;
69
+ const sorted = [...values].sort((a, b) => a - b);
70
+ const mid = Math.floor(sorted.length / 2);
71
+ if (sorted.length % 2 === 0) return (sorted[mid - 1] + sorted[mid]) / 2;
72
+ return sorted[mid];
73
+ }
74
+
75
+ function findOrphanedEntries(inputs, degrees) {
76
+ const insights = [];
77
+ const nodes = Array.isArray(inputs.graph?.nodes) ? inputs.graph.nodes : [];
78
+
79
+ for (const node of nodes) {
80
+ const nodePath = node.path || node.id || "";
81
+ if (!nodePath) continue;
82
+ const degree = degrees.get(nodePath) || 0;
83
+ if (degree === 0) {
84
+ const validation = validateGraphInsightArtifact({
85
+ schema_version: 1,
86
+ insight_id: `insight-orphaned-${nodePath.replace(/[^a-z0-9_-]/gi, "-")}`,
87
+ kind: "orphaned_entry_review",
88
+ title: `Review isolated entry: ${node.label || node.title || nodePath}`,
89
+ summary: `Entry has no graph edges and may need linking evidence or wikilink connections.`,
90
+ source_paths: [nodePath],
91
+ evidence_refs: [{ path: nodePath, reason: "isolated graph node with degree 0" }],
92
+ reasoning_hints: { graph_degree: degree, confidence: 0.4 },
93
+ review_status: "review_required",
94
+ review_only: true,
95
+ boundaries: { executed: false, mutates_sources: false, auto_promotes: false },
96
+ created_at: deriveDeterministicReferenceTime(inputs),
97
+ });
98
+ if (validation.valid) insights.push(validation.value);
99
+ }
100
+ }
101
+
102
+ return insights;
103
+ }
104
+
105
+ function findHighConnectivityEntries(inputs, degrees) {
106
+ const insights = [];
107
+ const nodeDegrees = [];
108
+ for (const [, degree] of degrees) {
109
+ nodeDegrees.push(degree);
110
+ }
111
+ const medianDegree = median(nodeDegrees);
112
+
113
+ for (const [nodePath, degree] of degrees) {
114
+ if (degree > medianDegree && degree >= 2) {
115
+ const validation = validateGraphInsightArtifact({
116
+ schema_version: 1,
117
+ insight_id: `insight-high-conn-${nodePath.replace(/[^a-z0-9_-]/gi, "-")}`,
118
+ kind: "high_connectivity_review",
119
+ title: `Review highly connected entry: ${nodePath}`,
120
+ summary: `Entry has ${degree} graph connections (above median ${medianDegree}). Review whether the links are meaningful and not over-connected.`,
121
+ source_paths: [nodePath],
122
+ evidence_refs: [{ path: nodePath, reason: `high degree ${degree} > median ${medianDegree}` }],
123
+ reasoning_hints: { graph_degree: degree, confidence: 0.5 },
124
+ review_status: "review_required",
125
+ review_only: true,
126
+ boundaries: { executed: false, mutates_sources: false, auto_promotes: false },
127
+ created_at: deriveDeterministicReferenceTime(inputs),
128
+ });
129
+ if (validation.valid) insights.push(validation.value);
130
+ }
131
+ }
132
+
133
+ return insights;
134
+ }
135
+
136
+ function findCuratorBacklogClusters(inputs) {
137
+ const insights = [];
138
+ const cards = Array.isArray(inputs.curatorQueue?.cards) ? inputs.curatorQueue.cards : [];
139
+ if (cards.length < 2) return insights;
140
+
141
+ const communities = Array.isArray(inputs.graph?.communities) ? inputs.graph.communities : [];
142
+ const pathToCommunities = new Map();
143
+ for (const community of communities) {
144
+ const members = Array.isArray(community.members) ? community.members : [];
145
+ for (const member of members) {
146
+ if (!pathToCommunities.has(member)) pathToCommunities.set(member, []);
147
+ pathToCommunities.get(member).push(community.id || community.label || "unknown");
148
+ }
149
+ }
150
+
151
+ for (let i = 0; i < cards.length; i++) {
152
+ for (let j = i + 1; j < cards.length; j++) {
153
+ const cardA = cards[i];
154
+ const cardB = cards[j];
155
+ const pathsA = Array.isArray(cardA.source_paths) ? cardA.source_paths : [];
156
+ const pathsB = Array.isArray(cardB.source_paths) ? cardB.source_paths : [];
157
+ const sharedPaths = pathsA.filter((p) => pathsB.includes(p));
158
+
159
+ const tagsA = Array.isArray(cardA.tags) ? cardA.tags : [];
160
+ const tagsB = Array.isArray(cardB.tags) ? cardB.tags : [];
161
+ const sharedTags = tagsA.filter((t) => tagsB.includes(t));
162
+
163
+ let sharedCommunities = 0;
164
+ for (const path of pathsA) {
165
+ const commsA = pathToCommunities.get(path) || [];
166
+ for (const pathB of pathsB) {
167
+ const commsB = pathToCommunities.get(pathB) || [];
168
+ sharedCommunities += commsA.filter((c) => commsB.includes(c)).length;
169
+ }
170
+ }
171
+
172
+ if (sharedPaths.length > 0 || sharedTags.length > 0 || sharedCommunities > 0) {
173
+ const clusterId = `insight-cluster-${cardA.id}-${cardB.id}`;
174
+ const validation = validateGraphInsightArtifact({
175
+ schema_version: 1,
176
+ insight_id: clusterId.slice(0, 120),
177
+ kind: "curator_backlog_cluster",
178
+ title: `Review related curator cards: ${cardA.title} / ${cardB.title}`,
179
+ summary: `Curator queue cards share ${sharedPaths.length} source path(s), ${sharedTags.length} tag(s), and ${sharedCommunities} community overlap(s). Consider whether they should be consolidated or ordered.`,
180
+ source_paths: [...new Set([...pathsA, ...pathsB])].sort(compareStrings),
181
+ evidence_refs: [
182
+ { path: cardA.artifact_path || cardA.id, reason: `shared with ${cardB.id}` },
183
+ { path: cardB.artifact_path || cardB.id, reason: `shared with ${cardA.id}` },
184
+ ].filter((ref) => ref.path),
185
+ reasoning_hints: {
186
+ community_overlap: sharedCommunities,
187
+ confidence: Math.min(1, (sharedPaths.length + sharedTags.length + sharedCommunities) * 0.2),
188
+ },
189
+ review_status: "review_required",
190
+ review_only: true,
191
+ boundaries: { executed: false, mutates_sources: false, auto_promotes: false },
192
+ created_at: deriveDeterministicReferenceTime(inputs),
193
+ });
194
+ if (validation.valid) insights.push(validation.value);
195
+ }
196
+ }
197
+ }
198
+
199
+ return insights;
200
+ }
201
+
202
+ function findStaleReviewCandidates(inputs) {
203
+ const insights = [];
204
+ const referenceTime = deriveDeterministicReferenceTime(inputs);
205
+ const candidates = Array.isArray(inputs.browserData?.candidates) ? inputs.browserData.candidates : [];
206
+
207
+ for (const candidate of candidates) {
208
+ const candidatePath = candidate.path || "";
209
+ const updatedAt = candidate.updated_at || candidate.created_at || "";
210
+ const daysStale = daysBetween(referenceTime, updatedAt);
211
+ if (daysStale >= 7) {
212
+ const validation = validateGraphInsightArtifact({
213
+ schema_version: 1,
214
+ insight_id: `insight-stale-${candidatePath.replace(/[^a-z0-9_-]/gi, "-")}`,
215
+ kind: "stale_review_candidate",
216
+ title: `Review stale candidate: ${candidate.title || candidatePath}`,
217
+ summary: `Candidate has not been updated for ${daysStale} day(s) since ${updatedAt}. Consider reviewing, promoting, or rejecting.`,
218
+ source_paths: [candidatePath],
219
+ evidence_refs: [{ path: candidatePath, reason: `stale for ${daysStale} days` }],
220
+ reasoning_hints: { days_since_update: daysStale, confidence: Math.min(1, daysStale / 30) },
221
+ review_status: "review_required",
222
+ review_only: true,
223
+ boundaries: { executed: false, mutates_sources: false, auto_promotes: false },
224
+ created_at: referenceTime,
225
+ });
226
+ if (validation.valid) insights.push(validation.value);
227
+ }
228
+ }
229
+
230
+ return insights;
231
+ }
232
+
233
+ export function sortInsightsDeterministically(insights) {
234
+ return [...insights].sort((left, right) => {
235
+ const kindCompare = compareStrings(left.kind, right.kind);
236
+ if (kindCompare !== 0) return kindCompare;
237
+ const idCompare = compareStrings(left.insight_id, right.insight_id);
238
+ if (idCompare !== 0) return idCompare;
239
+ return compareStrings(left.created_at, right.created_at);
240
+ });
241
+ }
242
+
243
+ export function analyzePersonalGraph(inputs) {
244
+ // deterministic graph reasoning: no Date.now(), no random, no remote APIs
245
+ const degrees = calculateNodeDegrees(inputs.graph);
246
+
247
+ const insights = [
248
+ ...findOrphanedEntries(inputs, degrees),
249
+ ...findHighConnectivityEntries(inputs, degrees),
250
+ ...findCuratorBacklogClusters(inputs),
251
+ ...findStaleReviewCandidates(inputs),
252
+ ];
253
+
254
+ return sortInsightsDeterministically(insights);
255
+ }
@@ -0,0 +1,130 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getGraphReasoningPaths, loadPersonalGraphReasoningInputs, readJsonLinesIfExists } from "./loader.js";
4
+ import { analyzePersonalGraph } from "./analyzer.js";
5
+ import { writeGraphInsightArtifacts, readGraphReasoningQueue, appendGraphReasoningAudit } from "./queue.js";
6
+ import { validateGraphInsightArtifact, normalizeStage4Path } from "./schema.js";
7
+
8
+ function compareStrings(left, right) {
9
+ return String(left ?? "").localeCompare(String(right ?? ""));
10
+ }
11
+
12
+ function summarizeInsight(insight, artifactPath) {
13
+ return {
14
+ id: insight.insight_id,
15
+ kind: insight.kind,
16
+ title: insight.title,
17
+ status: insight.review_status,
18
+ review_only: insight.review_only,
19
+ artifact_path: artifactPath,
20
+ source_paths: insight.source_paths,
21
+ reasoning_hints: insight.reasoning_hints,
22
+ created_at: insight.created_at,
23
+ updated_at: insight.updated_at,
24
+ };
25
+ }
26
+
27
+ function readInsightArtifacts(wikiRoot) {
28
+ const paths = getGraphReasoningPaths(wikiRoot);
29
+ if (!paths.insightsDir || !fs.existsSync(paths.insightsDir)) return [];
30
+ return fs.readdirSync(paths.insightsDir)
31
+ .filter((fileName) => fileName.endsWith(".json"))
32
+ .sort(compareStrings)
33
+ .flatMap((fileName) => {
34
+ const filePath = path.join(paths.insightsDir, fileName);
35
+ const artifactPath = `.system/stage4/graph-reasoning/insights/${fileName}`;
36
+ try {
37
+ const rawInsight = JSON.parse(fs.readFileSync(filePath, "utf-8"));
38
+ // Re-validate to ensure the artifact still meets schema and safety requirements
39
+ const validation = validateGraphInsightArtifact(rawInsight);
40
+ if (!validation.valid) return [];
41
+ // Ensure artifact path is safe (no absolute or traversal paths in source_paths)
42
+ const hasUnsafePaths = validation.value.source_paths.some((sp) => {
43
+ try {
44
+ normalizeStage4Path(sp);
45
+ return false;
46
+ } catch {
47
+ return true;
48
+ }
49
+ });
50
+ if (hasUnsafePaths) return [];
51
+ return [summarizeInsight(validation.value, artifactPath)];
52
+ } catch {
53
+ return [];
54
+ }
55
+ });
56
+ }
57
+
58
+ function readAuditSummary(wikiRoot) {
59
+ const paths = getGraphReasoningPaths(wikiRoot);
60
+ const actionCounts = {};
61
+ let auditCount = 0;
62
+ let lastRecord = null;
63
+
64
+ const records = readJsonLinesIfExists(paths.auditFile);
65
+ for (const record of records) {
66
+ const action = String(record.action || "unknown");
67
+ actionCounts[action] = (actionCounts[action] || 0) + 1;
68
+ auditCount += 1;
69
+ lastRecord = record;
70
+ }
71
+
72
+ return {
73
+ audit_count: auditCount,
74
+ action_counts: Object.fromEntries(Object.entries(actionCounts).sort(([left], [right]) => compareStrings(left, right))),
75
+ last_action: lastRecord?.action || "",
76
+ last_actor: lastRecord?.actor || "",
77
+ last_timestamp: lastRecord?.timestamp || "",
78
+ };
79
+ }
80
+
81
+ export function buildGraphReasoningBrowserData(wikiRoot) {
82
+ const queue = readGraphReasoningQueue(wikiRoot);
83
+ const insights = readInsightArtifacts(wikiRoot);
84
+ const auditSummary = readAuditSummary(wikiRoot);
85
+
86
+ return {
87
+ queue_cards: queue.cards.map((card) => ({
88
+ id: card.id,
89
+ kind: card.kind,
90
+ title: card.title,
91
+ status: card.status,
92
+ review_only: card.review_only,
93
+ artifact_path: card.artifact_path,
94
+ source_paths: card.source_paths,
95
+ reasoning_hints: card.reasoning_hints,
96
+ created_at: card.created_at,
97
+ updated_at: card.updated_at,
98
+ })),
99
+ insights,
100
+ summary: {
101
+ ...auditSummary,
102
+ insight_count: insights.length,
103
+ queue_card_count: queue.cards.length,
104
+ },
105
+ };
106
+ }
107
+
108
+ export function runGraphReasoningAnalysis(wikiRoot, options = {}) {
109
+ const inputs = loadPersonalGraphReasoningInputs(wikiRoot);
110
+ const insights = analyzePersonalGraph(inputs);
111
+ const writeResult = writeGraphInsightArtifacts(wikiRoot, insights, options);
112
+
113
+ if (!writeResult.success) return writeResult;
114
+
115
+ // Always write an audit record, even for zero-insight runs
116
+ const auditResult = appendGraphReasoningAudit(wikiRoot, {
117
+ subject_id: "analysis-run",
118
+ action: "run_analysis",
119
+ source_paths: [],
120
+ }, options);
121
+
122
+ return {
123
+ success: true,
124
+ wikiRoot,
125
+ insight_count: writeResult.count,
126
+ artifacts: writeResult.artifacts,
127
+ audit: auditResult.success ? auditResult.audit : null,
128
+ message: `Graph reasoning analysis completed: ${writeResult.count} insight(s) generated.`,
129
+ };
130
+ }
@@ -0,0 +1,35 @@
1
+ export {
2
+ validateGraphInsightArtifact,
3
+ validateGraphReasoningQueueCard,
4
+ validateGraphReasoningAuditRecord,
5
+ normalizeStage4Path,
6
+ redactGraphReasoningText,
7
+ GRAPH_REASONING_REVIEW_STATUS,
8
+ GRAPH_REASONING_KINDS,
9
+ } from "./schema.js";
10
+
11
+ export {
12
+ getGraphReasoningPaths,
13
+ loadPersonalGraphReasoningInputs,
14
+ readJsonFileIfExists,
15
+ readJsonLinesIfExists,
16
+ } from "./loader.js";
17
+
18
+ export {
19
+ analyzePersonalGraph,
20
+ calculateNodeDegrees,
21
+ deriveDeterministicReferenceTime,
22
+ sortInsightsDeterministically,
23
+ } from "./analyzer.js";
24
+
25
+ export {
26
+ ensureGraphReasoningStructure,
27
+ writeGraphInsightArtifacts,
28
+ readGraphReasoningQueue,
29
+ appendGraphReasoningAudit,
30
+ } from "./queue.js";
31
+
32
+ export {
33
+ buildGraphReasoningBrowserData,
34
+ runGraphReasoningAnalysis,
35
+ } from "./browser-data.js";
@@ -0,0 +1,122 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function getGraphReasoningPaths(wikiRoot) {
5
+ const systemDir = path.join(wikiRoot, ".system");
6
+ const root = path.join(systemDir, "stage4", "graph-reasoning");
7
+ return {
8
+ systemDir,
9
+ root,
10
+ queueFile: path.join(root, "queue.json"),
11
+ insightsDir: path.join(root, "insights"),
12
+ auditFile: path.join(root, "audit.jsonl"),
13
+ };
14
+ }
15
+
16
+ export function readJsonFileIfExists(filePath, fallback = null) {
17
+ try {
18
+ if (!fs.existsSync(filePath)) return fallback;
19
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
20
+ } catch {
21
+ return fallback;
22
+ }
23
+ }
24
+
25
+ export function readJsonLinesIfExists(filePath) {
26
+ try {
27
+ if (!fs.existsSync(filePath)) return [];
28
+ return fs.readFileSync(filePath, "utf-8")
29
+ .split(/\r?\n/)
30
+ .filter(Boolean)
31
+ .map((line) => {
32
+ try {
33
+ return JSON.parse(line);
34
+ } catch {
35
+ return null;
36
+ }
37
+ })
38
+ .filter(Boolean);
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ export function readCuratorArtifactsDir(dirPath) {
45
+ const artifacts = [];
46
+ try {
47
+ if (!fs.existsSync(dirPath)) return artifacts;
48
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
51
+ const filePath = path.join(dirPath, entry.name);
52
+ try {
53
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
54
+ // Basic validation: must be an object with required fields
55
+ if (content && typeof content === "object" && !Array.isArray(content) && content.id && content.title) {
56
+ artifacts.push({
57
+ ...content,
58
+ _artifact_path: `.system/curator/${path.basename(dirPath)}/${entry.name}`,
59
+ });
60
+ }
61
+ } catch {
62
+ // skip malformed artifacts
63
+ }
64
+ }
65
+ } catch {
66
+ // skip unreadable directories
67
+ }
68
+ return artifacts;
69
+ }
70
+
71
+ export function loadPersonalGraphReasoningInputs(wikiRoot) {
72
+ const loadErrors = [];
73
+
74
+ // Reads local .system/graph.json and .system/browser-data.json read-only.
75
+ const graphPath = path.join(wikiRoot, ".system", "graph.json");
76
+ const graph = readJsonFileIfExists(graphPath, { nodes: [], edges: [], communities: [], meta: {} });
77
+ if (!fs.existsSync(graphPath)) {
78
+ loadErrors.push("graph.json missing");
79
+ }
80
+
81
+ const browserDataPath = path.join(wikiRoot, ".system", "browser-data.json");
82
+ const browserData = readJsonFileIfExists(browserDataPath, {});
83
+ if (!fs.existsSync(browserDataPath)) {
84
+ loadErrors.push("browser-data.json missing");
85
+ }
86
+
87
+ const curatorQueue = readJsonFileIfExists(path.join(wikiRoot, ".system", "curator", "queue.json"), { cards: [] });
88
+
89
+ const curatorAudit = readJsonLinesIfExists(path.join(wikiRoot, ".system", "curator", "audit.jsonl"));
90
+
91
+ // Load Stage 3 curator artifact directories read-only
92
+ const lessonCandidates = readCuratorArtifactsDir(path.join(wikiRoot, ".system", "curator", "lesson-candidates"));
93
+ const consolidationProposals = readCuratorArtifactsDir(path.join(wikiRoot, ".system", "curator", "consolidation-proposals"));
94
+
95
+ const sourceFilePaths = [];
96
+ try {
97
+ const paths = [path.join(wikiRoot, "inbox"), path.join(wikiRoot, "problems"), path.join(wikiRoot, "editorial", "lessons")];
98
+ for (const dir of paths) {
99
+ if (!fs.existsSync(dir)) continue;
100
+ const entries = fs.readdirSync(dir, { withFileTypes: true, recursive: true });
101
+ for (const entry of entries) {
102
+ if (entry.isFile() && entry.name.endsWith(".md")) {
103
+ const relativePath = path.relative(wikiRoot, path.join(dir, entry.name)).replace(/\\/g, "/");
104
+ sourceFilePaths.push(relativePath);
105
+ }
106
+ }
107
+ }
108
+ } catch (error) {
109
+ loadErrors.push(`source scan error: ${error.message}`);
110
+ }
111
+
112
+ return {
113
+ graph: graph || { nodes: [], edges: [], communities: [], meta: {} },
114
+ browserData: browserData || {},
115
+ curatorQueue: curatorQueue || { cards: [] },
116
+ curatorAudit: curatorAudit || [],
117
+ lessonCandidates,
118
+ consolidationProposals,
119
+ sourceFilePaths: sourceFilePaths.sort((a, b) => a.localeCompare(b)),
120
+ loadErrors,
121
+ };
122
+ }
@@ -0,0 +1,154 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { validateGraphInsightArtifact, validateGraphReasoningAuditRecord, validateGraphReasoningQueueCard } from "./schema.js";
5
+ import { getGraphReasoningPaths } from "./loader.js";
6
+
7
+ const DEFAULT_BUILT_AT = "1970-01-01T00:00:00.000Z";
8
+
9
+ function compareStrings(left, right) {
10
+ return String(left ?? "").localeCompare(String(right ?? ""));
11
+ }
12
+
13
+ function atomicWriteJson(filePath, data) {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ const tmpFile = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
16
+ fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2));
17
+ fs.renameSync(tmpFile, filePath);
18
+ }
19
+
20
+ function safeId(value, fallback = "graph-reasoning") {
21
+ return String(value || fallback)
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9_-]+/g, "-")
24
+ .replace(/^-+|-+$/g, "")
25
+ .slice(0, 80) || fallback;
26
+ }
27
+
28
+ function shortHash(value) {
29
+ return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, 12);
30
+ }
31
+
32
+ function nowIso(options = {}) {
33
+ return options.now || new Date().toISOString();
34
+ }
35
+
36
+ function normalizeQueue(queue) {
37
+ const cards = [];
38
+ for (const card of Array.isArray(queue?.cards) ? queue.cards : []) {
39
+ const validation = validateGraphReasoningQueueCard(card);
40
+ if (validation.valid) cards.push(validation.value);
41
+ }
42
+ return {
43
+ version: 1,
44
+ cards: [...new Map(cards.map((card) => [card.id, card])).values()]
45
+ .sort((left, right) => compareStrings(`${left.created_at}:${left.id}`, `${right.created_at}:${right.id}`)),
46
+ meta: { built_at: queue?.meta?.built_at || DEFAULT_BUILT_AT },
47
+ };
48
+ }
49
+
50
+ function createEmptyQueue() {
51
+ return { version: 1, cards: [], meta: { built_at: DEFAULT_BUILT_AT } };
52
+ }
53
+
54
+ function writeQueue(wikiRoot, queue, options = {}) {
55
+ const normalized = normalizeQueue({ ...queue, meta: { built_at: nowIso(options) } });
56
+ atomicWriteJson(getGraphReasoningPaths(wikiRoot).queueFile, normalized);
57
+ return normalized;
58
+ }
59
+
60
+ function result(success, payload = {}) {
61
+ return { success, ...payload };
62
+ }
63
+
64
+ export function ensureGraphReasoningStructure(wikiRoot) {
65
+ const paths = getGraphReasoningPaths(wikiRoot);
66
+ fs.mkdirSync(paths.insightsDir, { recursive: true });
67
+ return paths;
68
+ }
69
+
70
+ export function readGraphReasoningQueue(wikiRoot) {
71
+ const paths = getGraphReasoningPaths(wikiRoot);
72
+ if (!fs.existsSync(paths.queueFile)) return createEmptyQueue();
73
+ try {
74
+ return normalizeQueue(JSON.parse(fs.readFileSync(paths.queueFile, "utf-8")));
75
+ } catch {
76
+ return createEmptyQueue();
77
+ }
78
+ }
79
+
80
+ export function appendGraphReasoningAudit(wikiRoot, record, options = {}) {
81
+ // Appends audit record to .system/stage4/graph-reasoning/audit.jsonl
82
+ const validation = validateGraphReasoningAuditRecord({
83
+ timestamp: nowIso(options),
84
+ actor: options.actor || "system",
85
+ executed: false,
86
+ ...record,
87
+ });
88
+ if (!validation.valid) return result(false, { error: validation.errors.join("; ") });
89
+ const paths = getGraphReasoningPaths(wikiRoot);
90
+ fs.mkdirSync(paths.root, { recursive: true });
91
+ fs.appendFileSync(paths.auditFile, `${JSON.stringify(validation.value)}\n`);
92
+ return result(true, { audit: validation.value });
93
+ }
94
+
95
+ function upsertCard(wikiRoot, card, options = {}) {
96
+ const validation = validateGraphReasoningQueueCard(card);
97
+ if (!validation.valid) return result(false, { error: validation.errors.join("; ") });
98
+ const queue = readGraphReasoningQueue(wikiRoot);
99
+ queue.cards = [...queue.cards.filter((item) => item.id !== validation.value.id), validation.value];
100
+ return result(true, { queue: writeQueue(wikiRoot, queue, options), card: validation.value });
101
+ }
102
+
103
+ export function writeGraphInsightArtifacts(wikiRoot, insights, options = {}) {
104
+ try {
105
+ const paths = ensureGraphReasoningStructure(wikiRoot);
106
+ const createdAt = nowIso(options);
107
+ const writtenArtifacts = [];
108
+ const writtenCards = [];
109
+ const auditRecords = [];
110
+
111
+ for (const insight of insights) {
112
+ const validation = validateGraphInsightArtifact(insight);
113
+ if (!validation.valid) continue;
114
+
115
+ const fileName = `${safeId(validation.value.insight_id)}-${shortHash(validation.value.insight_id)}.json`;
116
+ const filePath = path.join(paths.insightsDir, fileName);
117
+ const relativePath = `.system/stage4/graph-reasoning/insights/${fileName}`;
118
+ atomicWriteJson(filePath, validation.value);
119
+ writtenArtifacts.push(relativePath);
120
+
121
+ const sourcePaths = Array.isArray(validation.value.source_paths) ? validation.value.source_paths : [];
122
+ const cardResult = upsertCard(wikiRoot, {
123
+ id: `card-${validation.value.insight_id}`,
124
+ kind: validation.value.kind,
125
+ title: validation.value.title,
126
+ status: "review_required",
127
+ artifact_path: relativePath,
128
+ source_paths: sourcePaths,
129
+ reasoning_hints: validation.value.reasoning_hints,
130
+ created_at: createdAt,
131
+ updated_at: createdAt,
132
+ }, options);
133
+ if (cardResult.success) writtenCards.push(cardResult.card);
134
+
135
+ const auditResult = appendGraphReasoningAudit(wikiRoot, {
136
+ subject_id: validation.value.insight_id,
137
+ artifact_path: relativePath,
138
+ source_paths: sourcePaths,
139
+ action: "generate_insight",
140
+ }, options);
141
+ if (auditResult.success) auditRecords.push(auditResult.audit);
142
+ }
143
+
144
+ return result(true, {
145
+ artifacts: writtenArtifacts,
146
+ cards: writtenCards,
147
+ audits: auditRecords,
148
+ count: writtenArtifacts.length,
149
+ message: `Graph reasoning insights written: ${writtenArtifacts.length}`,
150
+ });
151
+ } catch (error) {
152
+ return result(false, { error: error.message });
153
+ }
154
+ }