@grainulation/silo 1.0.0 → 1.0.1
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.
- package/CODE_OF_CONDUCT.md +25 -0
- package/CONTRIBUTING.md +103 -0
- package/README.md +67 -59
- package/bin/silo.js +212 -86
- package/lib/analytics.js +26 -11
- package/lib/confluence.js +343 -0
- package/lib/graph.js +414 -0
- package/lib/import-export.js +29 -24
- package/lib/index.js +15 -9
- package/lib/packs.js +60 -36
- package/lib/search.js +24 -16
- package/lib/serve-mcp.js +391 -95
- package/lib/server.js +205 -110
- package/lib/store.js +34 -18
- package/lib/templates.js +28 -17
- package/package.json +7 -3
- package/packs/adr.json +219 -0
- package/packs/api-design.json +67 -14
- package/packs/architecture-decision.json +152 -0
- package/packs/architecture.json +45 -9
- package/packs/ci-cd.json +51 -11
- package/packs/compliance.json +70 -14
- package/packs/data-engineering.json +57 -12
- package/packs/frontend.json +56 -12
- package/packs/hackathon-best-ai.json +179 -0
- package/packs/hackathon-business-impact.json +180 -0
- package/packs/hackathon-innovation.json +210 -0
- package/packs/hackathon-most-innovative.json +179 -0
- package/packs/hackathon-most-rigorous.json +179 -0
- package/packs/hackathon-sprint-boost.json +173 -0
- package/packs/incident-postmortem.json +219 -0
- package/packs/migration.json +45 -9
- package/packs/observability.json +57 -12
- package/packs/security.json +61 -13
- package/packs/team-process.json +64 -13
- package/packs/testing.json +20 -4
- package/packs/vendor-eval.json +219 -0
- package/packs/vendor-evaluation.json +148 -0
package/lib/graph.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graph.js — Cross-sprint knowledge graph index
|
|
3
|
+
*
|
|
4
|
+
* Builds an in-memory graph linking claims across sprints and packs
|
|
5
|
+
* by topic, tags, conflicts, and content similarity. Enables discovery
|
|
6
|
+
* of related knowledge across the entire silo.
|
|
7
|
+
*
|
|
8
|
+
* Zero npm dependencies — uses only built-in Node.js modules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
const { Store } = require("./store.js");
|
|
14
|
+
const { Packs } = require("./packs.js");
|
|
15
|
+
|
|
16
|
+
class Graph {
|
|
17
|
+
constructor(store) {
|
|
18
|
+
this.store = store || new Store();
|
|
19
|
+
this.packs = new Packs(this.store);
|
|
20
|
+
this._nodes = new Map(); // claimKey -> node
|
|
21
|
+
this._edges = []; // { source, target, relation, weight }
|
|
22
|
+
this._topicIndex = new Map(); // topic -> [claimKey]
|
|
23
|
+
this._tagIndex = new Map(); // tag -> [claimKey]
|
|
24
|
+
this._built = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the knowledge graph from all stored collections and packs.
|
|
29
|
+
* Call this before querying.
|
|
30
|
+
*/
|
|
31
|
+
build() {
|
|
32
|
+
this._nodes.clear();
|
|
33
|
+
this._edges = [];
|
|
34
|
+
this._topicIndex.clear();
|
|
35
|
+
this._tagIndex.clear();
|
|
36
|
+
|
|
37
|
+
// Index stored collections
|
|
38
|
+
const collections = this.store.list();
|
|
39
|
+
for (const col of collections) {
|
|
40
|
+
const data = this.store.getClaims(col.id);
|
|
41
|
+
if (!data || !data.claims) continue;
|
|
42
|
+
this._indexClaims(data.claims, col.id, "collection");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Index built-in and local packs
|
|
46
|
+
const allPacks = this.packs.list();
|
|
47
|
+
for (const p of allPacks) {
|
|
48
|
+
const pack = this.packs.get(p.id);
|
|
49
|
+
if (!pack || !pack.claims) continue;
|
|
50
|
+
this._indexClaims(pack.claims, p.id, "pack");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Build edges from shared topics
|
|
54
|
+
for (const [, keys] of this._topicIndex) {
|
|
55
|
+
if (keys.length < 2) continue;
|
|
56
|
+
for (let i = 0; i < keys.length; i++) {
|
|
57
|
+
for (let j = i + 1; j < keys.length; j++) {
|
|
58
|
+
this._addEdge(keys[i], keys[j], "same-topic", 1.0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build edges from shared tags
|
|
64
|
+
for (const [, keys] of this._tagIndex) {
|
|
65
|
+
if (keys.length < 2) continue;
|
|
66
|
+
for (let i = 0; i < keys.length; i++) {
|
|
67
|
+
for (let j = i + 1; j < Math.min(keys.length, i + 50); j++) {
|
|
68
|
+
this._addEdge(keys[i], keys[j], "shared-tag", 0.5);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build edges from explicit conflicts
|
|
74
|
+
for (const [key, node] of this._nodes) {
|
|
75
|
+
const conflicts = node.claim.conflicts_with || [];
|
|
76
|
+
for (const conflictId of conflicts) {
|
|
77
|
+
// Find the conflicting claim in same source
|
|
78
|
+
const conflictKey = `${node.source}:${conflictId}`;
|
|
79
|
+
if (this._nodes.has(conflictKey)) {
|
|
80
|
+
this._addEdge(key, conflictKey, "conflict", 1.5);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this._built = true;
|
|
86
|
+
return this.stats();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Get graph statistics. */
|
|
90
|
+
stats() {
|
|
91
|
+
const sources = new Set();
|
|
92
|
+
for (const node of this._nodes.values()) {
|
|
93
|
+
sources.add(node.source);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
nodes: this._nodes.size,
|
|
97
|
+
edges: this._edges.length,
|
|
98
|
+
sources: sources.size,
|
|
99
|
+
topics: this._topicIndex.size,
|
|
100
|
+
tags: this._tagIndex.size,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Find claims related to a given claim by graph traversal.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} claimId - Claim ID to find neighbors for
|
|
108
|
+
* @param {object} opts
|
|
109
|
+
* @param {number} opts.depth - Traversal depth (default: 1)
|
|
110
|
+
* @param {number} opts.limit - Max results (default: 20)
|
|
111
|
+
* @param {string} opts.relation - Filter by relation type
|
|
112
|
+
* @returns {object[]} Related claims with relation info
|
|
113
|
+
*/
|
|
114
|
+
related(claimId, opts = {}) {
|
|
115
|
+
this._ensureBuilt();
|
|
116
|
+
const { depth = 1, limit = 20, relation } = opts;
|
|
117
|
+
|
|
118
|
+
// Find all node keys matching this claim ID
|
|
119
|
+
const startKeys = [];
|
|
120
|
+
for (const [key, node] of this._nodes) {
|
|
121
|
+
if (node.claim.id === claimId || key === claimId) {
|
|
122
|
+
startKeys.push(key);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (startKeys.length === 0) return [];
|
|
126
|
+
|
|
127
|
+
// BFS traversal
|
|
128
|
+
const visited = new Set(startKeys);
|
|
129
|
+
let frontier = new Set(startKeys);
|
|
130
|
+
const results = [];
|
|
131
|
+
|
|
132
|
+
for (let d = 0; d < depth; d++) {
|
|
133
|
+
const nextFrontier = new Set();
|
|
134
|
+
for (const edge of this._edges) {
|
|
135
|
+
if (relation && edge.relation !== relation) continue;
|
|
136
|
+
|
|
137
|
+
let neighbor = null;
|
|
138
|
+
if (frontier.has(edge.source) && !visited.has(edge.target)) {
|
|
139
|
+
neighbor = edge.target;
|
|
140
|
+
} else if (frontier.has(edge.target) && !visited.has(edge.source)) {
|
|
141
|
+
neighbor = edge.source;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (neighbor) {
|
|
145
|
+
visited.add(neighbor);
|
|
146
|
+
nextFrontier.add(neighbor);
|
|
147
|
+
const node = this._nodes.get(neighbor);
|
|
148
|
+
results.push({
|
|
149
|
+
claim: node.claim,
|
|
150
|
+
source: node.source,
|
|
151
|
+
sourceType: node.sourceType,
|
|
152
|
+
relation: edge.relation,
|
|
153
|
+
weight: edge.weight,
|
|
154
|
+
depth: d + 1,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
frontier = nextFrontier;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
results.sort((a, b) => b.weight - a.weight);
|
|
162
|
+
return results.slice(0, limit);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find all claims for a topic across all sources.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} topic - Topic string (case-insensitive)
|
|
169
|
+
* @param {number} limit - Max results
|
|
170
|
+
* @returns {object[]}
|
|
171
|
+
*/
|
|
172
|
+
byTopic(topic, limit = 50) {
|
|
173
|
+
this._ensureBuilt();
|
|
174
|
+
const normalized = topic.toLowerCase().trim();
|
|
175
|
+
const results = [];
|
|
176
|
+
|
|
177
|
+
for (const [topicKey, keys] of this._topicIndex) {
|
|
178
|
+
if (topicKey.includes(normalized) || normalized.includes(topicKey)) {
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
const node = this._nodes.get(key);
|
|
181
|
+
results.push({
|
|
182
|
+
claim: node.claim,
|
|
183
|
+
source: node.source,
|
|
184
|
+
sourceType: node.sourceType,
|
|
185
|
+
topic: topicKey,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results.slice(0, limit);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Find all claims with a given tag across all sources.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} tag - Tag string
|
|
198
|
+
* @param {number} limit - Max results
|
|
199
|
+
* @returns {object[]}
|
|
200
|
+
*/
|
|
201
|
+
byTag(tag, limit = 50) {
|
|
202
|
+
this._ensureBuilt();
|
|
203
|
+
const keys = this._tagIndex.get(tag.toLowerCase()) || [];
|
|
204
|
+
return keys.slice(0, limit).map((key) => {
|
|
205
|
+
const node = this._nodes.get(key);
|
|
206
|
+
return {
|
|
207
|
+
claim: node.claim,
|
|
208
|
+
source: node.source,
|
|
209
|
+
sourceType: node.sourceType,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Find clusters of densely connected claims.
|
|
216
|
+
* Returns groups of claims that share multiple connections.
|
|
217
|
+
*
|
|
218
|
+
* @param {number} minSize - Minimum cluster size (default: 3)
|
|
219
|
+
* @returns {object[]} Array of { topic, claims, edgeCount }
|
|
220
|
+
*/
|
|
221
|
+
clusters(minSize = 3) {
|
|
222
|
+
this._ensureBuilt();
|
|
223
|
+
const clusters = [];
|
|
224
|
+
|
|
225
|
+
for (const [topic, keys] of this._topicIndex) {
|
|
226
|
+
if (keys.length < minSize) continue;
|
|
227
|
+
|
|
228
|
+
// Count internal edges
|
|
229
|
+
const keySet = new Set(keys);
|
|
230
|
+
let edgeCount = 0;
|
|
231
|
+
for (const edge of this._edges) {
|
|
232
|
+
if (keySet.has(edge.source) && keySet.has(edge.target)) {
|
|
233
|
+
edgeCount++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const claims = keys.map((k) => {
|
|
238
|
+
const n = this._nodes.get(k);
|
|
239
|
+
return { claim: n.claim, source: n.source, sourceType: n.sourceType };
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
clusters.push({ topic, claims, claimCount: keys.length, edgeCount });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
clusters.sort((a, b) => b.edgeCount - a.edgeCount);
|
|
246
|
+
return clusters;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Search for related prior art across all sprints and packs.
|
|
251
|
+
* Combines text matching with graph traversal to find claims
|
|
252
|
+
* relevant to a query, ranked by combined text + graph relevance.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} query - Free-text search query
|
|
255
|
+
* @param {object} opts
|
|
256
|
+
* @param {number} opts.limit - Max results (default: 20)
|
|
257
|
+
* @param {string} opts.type - Filter by claim type
|
|
258
|
+
* @param {string} opts.sourceType - Filter by source type ('collection' or 'pack')
|
|
259
|
+
* @returns {object[]} Ranked results with claim, source, score, and relations
|
|
260
|
+
*/
|
|
261
|
+
search(query, opts = {}) {
|
|
262
|
+
this._ensureBuilt();
|
|
263
|
+
const { limit = 20, type, sourceType } = opts;
|
|
264
|
+
const tokens = query
|
|
265
|
+
.toLowerCase()
|
|
266
|
+
.split(/\s+/)
|
|
267
|
+
.filter((t) => t.length > 1);
|
|
268
|
+
if (tokens.length === 0) return [];
|
|
269
|
+
|
|
270
|
+
const scored = new Map(); // claimKey -> { node, textScore, graphScore }
|
|
271
|
+
|
|
272
|
+
// Phase 1: Text matching across all nodes
|
|
273
|
+
for (const [key, node] of this._nodes) {
|
|
274
|
+
if (type && node.claim.type !== type) continue;
|
|
275
|
+
if (sourceType && node.sourceType !== sourceType) continue;
|
|
276
|
+
|
|
277
|
+
const searchable = [
|
|
278
|
+
node.claim.topic || "",
|
|
279
|
+
node.claim.content || "",
|
|
280
|
+
(node.claim.tags || []).join(" "),
|
|
281
|
+
node.source,
|
|
282
|
+
]
|
|
283
|
+
.join(" ")
|
|
284
|
+
.toLowerCase();
|
|
285
|
+
|
|
286
|
+
let textScore = 0;
|
|
287
|
+
for (const token of tokens) {
|
|
288
|
+
if (searchable.includes(token)) {
|
|
289
|
+
textScore += 1;
|
|
290
|
+
// Bonus for topic match
|
|
291
|
+
if ((node.claim.topic || "").toLowerCase().includes(token)) {
|
|
292
|
+
textScore += 0.5;
|
|
293
|
+
}
|
|
294
|
+
// Bonus for tag match
|
|
295
|
+
if (
|
|
296
|
+
(node.claim.tags || []).some((t) => t.toLowerCase().includes(token))
|
|
297
|
+
) {
|
|
298
|
+
textScore += 0.3;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (textScore > 0) {
|
|
304
|
+
scored.set(key, { node, textScore, graphScore: 0 });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Phase 2: Graph boost — text matches with many graph connections score higher
|
|
309
|
+
for (const [key, entry] of scored) {
|
|
310
|
+
let graphScore = 0;
|
|
311
|
+
for (const edge of this._edges) {
|
|
312
|
+
if (edge.source === key || edge.target === key) {
|
|
313
|
+
const neighborKey = edge.source === key ? edge.target : edge.source;
|
|
314
|
+
// Boost if neighbor also matched text search
|
|
315
|
+
if (scored.has(neighborKey)) {
|
|
316
|
+
graphScore += edge.weight * 0.5;
|
|
317
|
+
} else {
|
|
318
|
+
graphScore += edge.weight * 0.1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
entry.graphScore = graphScore;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Combine and rank
|
|
326
|
+
const results = [];
|
|
327
|
+
for (const [key, entry] of scored) {
|
|
328
|
+
const combinedScore = entry.textScore + entry.graphScore;
|
|
329
|
+
results.push({
|
|
330
|
+
claim: entry.node.claim,
|
|
331
|
+
source: entry.node.source,
|
|
332
|
+
sourceType: entry.node.sourceType,
|
|
333
|
+
score: Math.round(combinedScore * 100) / 100,
|
|
334
|
+
textScore: Math.round(entry.textScore * 100) / 100,
|
|
335
|
+
graphScore: Math.round(entry.graphScore * 100) / 100,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
results.sort((a, b) => b.score - a.score);
|
|
340
|
+
return results.slice(0, limit);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Export the graph as a portable JSON structure.
|
|
345
|
+
*/
|
|
346
|
+
toJSON() {
|
|
347
|
+
this._ensureBuilt();
|
|
348
|
+
const nodes = [];
|
|
349
|
+
for (const [key, node] of this._nodes) {
|
|
350
|
+
nodes.push({
|
|
351
|
+
key,
|
|
352
|
+
id: node.claim.id,
|
|
353
|
+
type: node.claim.type,
|
|
354
|
+
topic: node.claim.topic,
|
|
355
|
+
source: node.source,
|
|
356
|
+
sourceType: node.sourceType,
|
|
357
|
+
tags: node.claim.tags || [],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
nodes,
|
|
362
|
+
edges: this._edges,
|
|
363
|
+
stats: this.stats(),
|
|
364
|
+
builtAt: new Date().toISOString(),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Internals ──────────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
_indexClaims(claims, source, sourceType) {
|
|
371
|
+
for (const claim of claims) {
|
|
372
|
+
const key = `${source}:${claim.id || ""}`;
|
|
373
|
+
if (this._nodes.has(key)) continue;
|
|
374
|
+
|
|
375
|
+
this._nodes.set(key, { claim, source, sourceType });
|
|
376
|
+
|
|
377
|
+
// Index by topic
|
|
378
|
+
if (claim.topic) {
|
|
379
|
+
const topic = claim.topic.toLowerCase().trim();
|
|
380
|
+
if (!this._topicIndex.has(topic)) this._topicIndex.set(topic, []);
|
|
381
|
+
this._topicIndex.get(topic).push(key);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Index by tags
|
|
385
|
+
for (const tag of claim.tags || []) {
|
|
386
|
+
const t = tag.toLowerCase();
|
|
387
|
+
if (!this._tagIndex.has(t)) this._tagIndex.set(t, []);
|
|
388
|
+
this._tagIndex.get(t).push(key);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_addEdge(source, target, relation, weight) {
|
|
394
|
+
// Avoid duplicate edges
|
|
395
|
+
const exists = this._edges.some(
|
|
396
|
+
(e) =>
|
|
397
|
+
(e.source === source &&
|
|
398
|
+
e.target === target &&
|
|
399
|
+
e.relation === relation) ||
|
|
400
|
+
(e.source === target && e.target === source && e.relation === relation),
|
|
401
|
+
);
|
|
402
|
+
if (!exists) {
|
|
403
|
+
this._edges.push({ source, target, relation, weight });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_ensureBuilt() {
|
|
408
|
+
if (!this._built) {
|
|
409
|
+
this.build();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = { Graph };
|
package/lib/import-export.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* (or built-in pack) and merge them into a sprint's claims.json.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const fs = require(
|
|
9
|
-
const path = require(
|
|
10
|
-
const { Store } = require(
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
const { Store } = require("./store.js");
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Normalize a claim to wheat's canonical schema.
|
|
@@ -18,33 +18,36 @@ function normalizeClaim(claim) {
|
|
|
18
18
|
const normalized = { ...claim };
|
|
19
19
|
|
|
20
20
|
// Map legacy field names to wheat-canonical names
|
|
21
|
-
if (
|
|
21
|
+
if ("tier" in normalized && !("evidence" in normalized)) {
|
|
22
22
|
normalized.evidence = normalized.tier;
|
|
23
23
|
}
|
|
24
24
|
delete normalized.tier;
|
|
25
25
|
|
|
26
|
-
if (
|
|
26
|
+
if ("text" in normalized && !("content" in normalized)) {
|
|
27
27
|
normalized.content = normalized.text;
|
|
28
28
|
}
|
|
29
29
|
delete normalized.text;
|
|
30
30
|
|
|
31
31
|
// Ensure all required wheat fields exist
|
|
32
32
|
normalized.id = normalized.id || null;
|
|
33
|
-
normalized.type = normalized.type ||
|
|
34
|
-
normalized.topic = normalized.topic ||
|
|
35
|
-
normalized.content = normalized.content ||
|
|
36
|
-
normalized.evidence = normalized.evidence ||
|
|
37
|
-
normalized.status = normalized.status ||
|
|
38
|
-
normalized.phase_added = normalized.phase_added ||
|
|
33
|
+
normalized.type = normalized.type || "factual";
|
|
34
|
+
normalized.topic = normalized.topic || "";
|
|
35
|
+
normalized.content = normalized.content || "";
|
|
36
|
+
normalized.evidence = normalized.evidence || "stated";
|
|
37
|
+
normalized.status = normalized.status || "active";
|
|
38
|
+
normalized.phase_added = normalized.phase_added || "import";
|
|
39
39
|
normalized.timestamp = normalized.timestamp || new Date().toISOString();
|
|
40
40
|
normalized.conflicts_with = normalized.conflicts_with || [];
|
|
41
41
|
normalized.resolved_by = normalized.resolved_by || null;
|
|
42
42
|
normalized.tags = normalized.tags || [];
|
|
43
43
|
|
|
44
44
|
// Normalize source to object form
|
|
45
|
-
if (!normalized.source || typeof normalized.source ===
|
|
45
|
+
if (!normalized.source || typeof normalized.source === "string") {
|
|
46
46
|
normalized.source = {
|
|
47
|
-
origin:
|
|
47
|
+
origin:
|
|
48
|
+
typeof normalized.source === "string"
|
|
49
|
+
? normalized.source
|
|
50
|
+
: "silo-import",
|
|
48
51
|
artifact: null,
|
|
49
52
|
connector: null,
|
|
50
53
|
};
|
|
@@ -71,7 +74,7 @@ class ImportExport {
|
|
|
71
74
|
* @param {boolean} opts.dryRun - If true, return what would be imported without writing
|
|
72
75
|
*/
|
|
73
76
|
pull(source, targetPath, opts = {}) {
|
|
74
|
-
const { prefix =
|
|
77
|
+
const { prefix = "imp", types, ids, dryRun = false } = opts;
|
|
75
78
|
|
|
76
79
|
// Resolve source: try silo store first, then built-in packs
|
|
77
80
|
let sourceClaims = this._resolveSource(source);
|
|
@@ -96,7 +99,7 @@ class ImportExport {
|
|
|
96
99
|
// Re-prefix claim IDs
|
|
97
100
|
const imported = sourceClaims.map((claim, i) => ({
|
|
98
101
|
...claim,
|
|
99
|
-
id: `${prefix}${String(i + 1).padStart(3,
|
|
102
|
+
id: `${prefix}${String(i + 1).padStart(3, "0")}`,
|
|
100
103
|
importedFrom: source,
|
|
101
104
|
importedAt: new Date().toISOString(),
|
|
102
105
|
}));
|
|
@@ -108,25 +111,27 @@ class ImportExport {
|
|
|
108
111
|
// Read existing target or create empty
|
|
109
112
|
let existing = [];
|
|
110
113
|
if (fs.existsSync(targetPath)) {
|
|
111
|
-
const raw = JSON.parse(fs.readFileSync(targetPath,
|
|
114
|
+
const raw = JSON.parse(fs.readFileSync(targetPath, "utf-8"));
|
|
112
115
|
existing = Array.isArray(raw) ? raw : raw.claims || [];
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
// Deduplicate by content (wheat-canonical field)
|
|
116
119
|
const existingTexts = new Set(
|
|
117
|
-
existing.map((c) => (c.content || c.text ||
|
|
120
|
+
existing.map((c) => (c.content || c.text || "").toLowerCase()),
|
|
118
121
|
);
|
|
119
122
|
const deduped = imported.filter(
|
|
120
|
-
(c) => !existingTexts.has((c.content ||
|
|
123
|
+
(c) => !existingTexts.has((c.content || "").toLowerCase()),
|
|
121
124
|
);
|
|
122
125
|
|
|
123
126
|
const merged = [...existing, ...deduped];
|
|
124
|
-
const output = Array.isArray(
|
|
127
|
+
const output = Array.isArray(
|
|
128
|
+
JSON.parse(fs.readFileSync(targetPath, "utf-8") || "[]"),
|
|
129
|
+
)
|
|
125
130
|
? merged
|
|
126
131
|
: { claims: merged };
|
|
127
132
|
|
|
128
|
-
const tmp = targetPath +
|
|
129
|
-
fs.writeFileSync(tmp, JSON.stringify(output, null, 2) +
|
|
133
|
+
const tmp = targetPath + ".tmp." + process.pid;
|
|
134
|
+
fs.writeFileSync(tmp, JSON.stringify(output, null, 2) + "\n", "utf-8");
|
|
130
135
|
fs.renameSync(tmp, targetPath);
|
|
131
136
|
|
|
132
137
|
return {
|
|
@@ -148,7 +153,7 @@ class ImportExport {
|
|
|
148
153
|
throw new Error(`Claims file not found: ${sourcePath}`);
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
const raw = JSON.parse(fs.readFileSync(sourcePath,
|
|
156
|
+
const raw = JSON.parse(fs.readFileSync(sourcePath, "utf-8"));
|
|
152
157
|
const claims = Array.isArray(raw) ? raw : raw.claims || [];
|
|
153
158
|
|
|
154
159
|
return this.store.storeClaims(name, claims, meta);
|
|
@@ -161,9 +166,9 @@ class ImportExport {
|
|
|
161
166
|
if (stored) return stored.claims;
|
|
162
167
|
|
|
163
168
|
// Try built-in packs
|
|
164
|
-
const packPath = path.join(__dirname,
|
|
169
|
+
const packPath = path.join(__dirname, "..", "packs", `${source}.json`);
|
|
165
170
|
if (fs.existsSync(packPath)) {
|
|
166
|
-
const pack = JSON.parse(fs.readFileSync(packPath,
|
|
171
|
+
const pack = JSON.parse(fs.readFileSync(packPath, "utf-8"));
|
|
167
172
|
return pack.claims || [];
|
|
168
173
|
}
|
|
169
174
|
|
package/lib/index.js
CHANGED
|
@@ -4,18 +4,22 @@
|
|
|
4
4
|
* Re-exports all public modules so consumers can import from a single path.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const { readFileSync } = require(
|
|
8
|
-
const { join } = require(
|
|
9
|
-
const { Store, DEFAULT_SILO_DIR } = require(
|
|
10
|
-
const { Search } = require(
|
|
11
|
-
const { Packs } = require(
|
|
12
|
-
const { ImportExport } = require(
|
|
13
|
-
const { Templates } = require(
|
|
7
|
+
const { readFileSync } = require("node:fs");
|
|
8
|
+
const { join } = require("node:path");
|
|
9
|
+
const { Store, DEFAULT_SILO_DIR } = require("./store.js");
|
|
10
|
+
const { Search } = require("./search.js");
|
|
11
|
+
const { Packs } = require("./packs.js");
|
|
12
|
+
const { ImportExport } = require("./import-export.js");
|
|
13
|
+
const { Templates } = require("./templates.js");
|
|
14
|
+
const { Graph } = require("./graph.js");
|
|
15
|
+
const { Confluence } = require("./confluence.js");
|
|
14
16
|
|
|
15
|
-
const pkg = JSON.parse(
|
|
17
|
+
const pkg = JSON.parse(
|
|
18
|
+
readFileSync(join(__dirname, "..", "package.json"), "utf-8"),
|
|
19
|
+
);
|
|
16
20
|
|
|
17
21
|
module.exports = {
|
|
18
|
-
name:
|
|
22
|
+
name: "silo",
|
|
19
23
|
version: pkg.version,
|
|
20
24
|
description: pkg.description,
|
|
21
25
|
|
|
@@ -25,4 +29,6 @@ module.exports = {
|
|
|
25
29
|
Packs,
|
|
26
30
|
ImportExport,
|
|
27
31
|
Templates,
|
|
32
|
+
Graph,
|
|
33
|
+
Confluence,
|
|
28
34
|
};
|