@grainulation/silo 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ Initial release.
6
+
7
+ - 11 built-in knowledge packs (compliance, architecture, security, testing, and more)
8
+ - Web pack browser with search and claim table
9
+ - Store, Search, ImportExport, Templates, and Packs library classes
10
+ - `silo pull`, `silo store`, `silo search`, `silo publish` CLI commands
11
+ - File-watching with automatic state refresh
12
+ - SSE live-reload
13
+ - Zero runtime dependencies
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 grainulation contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @grainulation/silo
2
+
3
+ Reusable knowledge for research sprints. Grab a starter pack in one command:
4
+
5
+ ```bash
6
+ npx @grainulation/silo pull compliance --into ./claims.json
7
+ ```
8
+
9
+ ## What is Silo?
10
+
11
+ Silo stores and shares research knowledge across projects and teams. Instead of starting every sprint from scratch, pull in battle-tested constraint sets, risk patterns, and decision templates.
12
+
13
+ - **Shared claim libraries** -- reusable constraint sets (HIPAA, SOC 2, GDPR, etc.)
14
+ - **Community sprint templates** -- pre-built research questions with seeded claims
15
+ - **Claim import/export** -- pull claims from one sprint into another
16
+ - **Knowledge packs** -- version-controlled bundles teams can subscribe to
17
+ - **Full-text search** -- find relevant claims across everything you've stored
18
+
19
+ Zero dependencies. Filesystem-based storage. Works offline.
20
+
21
+ ## Built-in Packs
22
+
23
+ | Pack | Claims | What's inside |
24
+ |------|--------|---------------|
25
+ | `api-design` | 13 | REST conventions, versioning, pagination, error formats, GraphQL tradeoffs |
26
+ | `architecture` | 12 | Monolith vs micro, build vs buy, SQL vs NoSQL decision claims |
27
+ | `ci-cd` | 12 | CI/CD pipeline patterns, caching, rollback strategies |
28
+ | `compliance` | 14 | HIPAA, SOC 2, GDPR constraint sets with regulatory citations |
29
+ | `data-engineering` | 12 | ETL patterns, data quality, warehouse design |
30
+ | `frontend` | 12 | Frontend architecture, performance, accessibility patterns |
31
+ | `migration` | 10 | Database/cloud/framework migration risks and patterns |
32
+ | `observability` | 12 | Logging, metrics, tracing, alerting patterns |
33
+ | `security` | 12 | Security constraints, threat models, authentication patterns |
34
+ | `team-process` | 12 | Team workflow, code review, incident response patterns |
35
+ | `testing` | 10 | Testing strategies, coverage, test architecture |
36
+
37
+ ## Quick Start
38
+
39
+ ```bash
40
+ # See available packs
41
+ silo packs
42
+
43
+ # Pull compliance constraints into your sprint
44
+ silo pull compliance --into ./claims.json
45
+
46
+ # Pull only GDPR constraints
47
+ silo pull compliance --into ./claims.json --type constraint
48
+
49
+ # Search across stored claims
50
+ silo search "encryption at rest"
51
+
52
+ # Store your sprint's findings for reuse
53
+ silo store "q4-migration-findings" --from ./claims.json
54
+
55
+ # List everything in your silo
56
+ silo list
57
+ ```
58
+
59
+ ## CLI Reference
60
+
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `silo list` | List all stored collections |
64
+ | `silo pull <pack> --into <file>` | Pull claims into a claims file |
65
+ | `silo store <name> --from <file>` | Store claims from a sprint |
66
+ | `silo search <query>` | Full-text search across claims |
67
+ | `silo packs` | List available knowledge packs |
68
+ | `silo templates` | List saved sprint templates |
69
+ | `silo publish <name> --collections <ids>` | Bundle collections into a pack |
70
+ | `silo install <file>` | Install a pack from a JSON file |
71
+
72
+ ## How It Works
73
+
74
+ Silo uses your filesystem for storage (`~/.silo` by default). No database, no network calls, no accounts. Claims are JSON files organized by collection, indexed for search.
75
+
76
+ When you `pull`, Silo:
77
+ 1. Resolves the source (built-in pack or stored collection)
78
+ 2. Re-prefixes claim IDs to avoid collisions
79
+ 3. Deduplicates against existing claims in the target
80
+ 4. Merges into your claims.json
81
+
82
+ When you `store`, Silo:
83
+ 1. Reads your sprint's claims.json
84
+ 2. Hashes the content for versioning
85
+ 3. Saves to `~/.silo/claims/` with metadata
86
+ 4. Updates the search index
87
+
88
+ ## Programmatic API
89
+
90
+ ```js
91
+ const { Store } = require('@grainulation/silo/lib/store');
92
+ const { Search } = require('@grainulation/silo/lib/search');
93
+ const { ImportExport } = require('@grainulation/silo/lib/import-export');
94
+
95
+ const store = new Store().init();
96
+ const search = new Search(store);
97
+ const io = new ImportExport(store);
98
+
99
+ // Search for encryption-related claims
100
+ const results = search.query('encryption', { type: 'constraint' });
101
+
102
+ // Pull a pack programmatically
103
+ io.pull('compliance', './claims.json');
104
+
105
+ // Store findings
106
+ io.push('./claims.json', 'my-sprint-findings');
107
+ ```
108
+
109
+ ## License
110
+
111
+ MIT
package/bin/silo.js ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * silo — CLI for storing and sharing reusable research knowledge
5
+ *
6
+ * Usage:
7
+ * silo list List all stored collections and packs
8
+ * silo pull <pack> --into <file> Pull claims from a pack into a claims file
9
+ * silo store <name> --from <file> Store claims from a file into the silo
10
+ * silo search <query> Search across all stored claims
11
+ * silo publish <name> --collections <ids...> Bundle collections into a pack
12
+ * silo packs List available knowledge packs
13
+ * silo templates List available sprint templates
14
+ * silo serve [--port 9095] Start the knowledge browser UI
15
+ * silo analyze Run cross-library analytics (requires harvest)
16
+ * silo serve-mcp Start the MCP server on stdio
17
+ */
18
+
19
+ const { Store } = require('../lib/store.js');
20
+ const { Search } = require('../lib/search.js');
21
+ const { ImportExport } = require('../lib/import-export.js');
22
+ const { Templates } = require('../lib/templates.js');
23
+ const { Packs } = require('../lib/packs.js');
24
+
25
+ // ── --version / -v (before verbose check) ──
26
+ if (process.argv.includes('--version') || (process.argv.includes('-v') && process.argv.length === 3)) {
27
+ const pkg = require('../package.json');
28
+ process.stdout.write(pkg.version + '\n');
29
+ process.exit(0);
30
+ }
31
+
32
+ const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
33
+ function vlog(...a) {
34
+ if (!verbose) return;
35
+ const ts = new Date().toISOString();
36
+ process.stderr.write(`[${ts}] silo: ${a.join(' ')}\n`);
37
+ }
38
+
39
+ const store = new Store();
40
+ const search = new Search(store);
41
+ const io = new ImportExport(store);
42
+ const templates = new Templates(store);
43
+ const packs = new Packs(store);
44
+
45
+ const args = process.argv.slice(2);
46
+ const command = args[0];
47
+
48
+ vlog('startup', `command=${command || '(none)'}`, `cwd=${process.cwd()}`);
49
+
50
+ const jsonMode = args.includes('--json');
51
+
52
+ function flag(name) {
53
+ const idx = args.indexOf(`--${name}`);
54
+ if (idx === -1) return null;
55
+ return args[idx + 1] || true;
56
+ }
57
+
58
+ function flagList(name) {
59
+ const idx = args.indexOf(`--${name}`);
60
+ if (idx === -1) return [];
61
+ const values = [];
62
+ for (let i = idx + 1; i < args.length; i++) {
63
+ if (args[i].startsWith('--')) break;
64
+ values.push(args[i]);
65
+ }
66
+ return values;
67
+ }
68
+
69
+ function print(obj) {
70
+ if (typeof obj === 'string') {
71
+ process.stdout.write(obj + '\n');
72
+ } else {
73
+ process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
74
+ }
75
+ }
76
+
77
+ function usage() {
78
+ print(`silo -- reusable knowledge for research sprints
79
+
80
+ Commands:
81
+ list List all stored collections and packs
82
+ pull <pack> --into <file> [--filter <ids>] Pull claims (optionally filter by ID)
83
+ store <name> --from <file> Store claims from a file into the silo
84
+ search <query> [--type <type>] Search across all stored claims
85
+ publish <name> --collections <ids...> Bundle collections into a pack
86
+ packs List available knowledge packs
87
+ templates List available sprint templates
88
+ install <file> Install a pack from a file
89
+ analyze Run cross-library analytics (requires harvest)
90
+ serve [--port 9095] Start the knowledge browser UI
91
+ serve-mcp Start the MCP server on stdio
92
+
93
+ Examples:
94
+ silo pull compliance --into ./claims.json
95
+ silo search "encryption" --type constraint
96
+ silo store "my-findings" --from ./claims.json
97
+ silo serve --port 9095
98
+ silo packs`);
99
+ }
100
+
101
+ try {
102
+ switch (command) {
103
+ case 'list': {
104
+ const collections = store.list();
105
+ if (jsonMode) {
106
+ print(JSON.stringify(collections));
107
+ break;
108
+ }
109
+ if (collections.length === 0) {
110
+ print('No stored collections. Use "silo store" to save claims or "silo packs" to see built-in packs.');
111
+ } else {
112
+ print('Stored collections:\n');
113
+ for (const c of collections) {
114
+ print(` ${c.id} (${c.type || 'claims'}, ${c.claimCount} claims) ${c.storedAt || ''}`);
115
+ }
116
+ }
117
+ break;
118
+ }
119
+
120
+ case 'pull': {
121
+ const source = args[1];
122
+ const into = flag('into');
123
+ if (!source || !into) {
124
+ print('Usage: silo pull <pack> --into <file>');
125
+ process.exit(1);
126
+ }
127
+ const dryRun = args.includes('--dry-run');
128
+ const types = flagList('type');
129
+ const filterIds = flag('filter');
130
+ const ids = filterIds ? filterIds.split(',').map((s) => s.trim()) : undefined;
131
+ const result = io.pull(source, into, { types: types.length ? types : undefined, ids, dryRun });
132
+ if (dryRun) {
133
+ print(`Dry run: would import ${result.wouldImport} claims`);
134
+ print(result.claims);
135
+ } else {
136
+ print(`Imported ${result.imported} claims into ${into} (${result.skippedDuplicates} duplicates skipped, ${result.totalClaims} total)`);
137
+ }
138
+ break;
139
+ }
140
+
141
+ case 'store': {
142
+ const name = args[1];
143
+ const from = flag('from');
144
+ if (!name || !from) {
145
+ print('Usage: silo store <name> --from <file>');
146
+ process.exit(1);
147
+ }
148
+ const result = io.push(from, name);
149
+ print(`Stored "${name}" (${result.claimCount} claims, hash: ${result.hash})`);
150
+ break;
151
+ }
152
+
153
+ case 'search': {
154
+ const query = args.slice(1).filter((a) => !a.startsWith('--')).join(' ');
155
+ if (!query) {
156
+ print('Usage: silo search <query> [--type <type>] [--evidence <tier>]');
157
+ process.exit(1);
158
+ }
159
+ const type = flag('type');
160
+ const tier = flag('tier');
161
+ const results = search.query(query, { type, tier });
162
+ if (jsonMode) {
163
+ print(JSON.stringify(results));
164
+ break;
165
+ }
166
+ if (results.length === 0) {
167
+ print('No matches found.');
168
+ } else {
169
+ print(`${results.length} result(s):\n`);
170
+ for (const r of results) {
171
+ const text = r.claim.content || r.claim.text || '';
172
+ const tier = r.claim.evidence || r.claim.tier || 'unknown';
173
+ print(` [${r.claim.id}] (${r.claim.type}, ${tier}) ${text.slice(0, 120)}${text.length > 120 ? '...' : ''}`);
174
+ print(` from: ${r.collection} score: ${r.score}\n`);
175
+ }
176
+ }
177
+ break;
178
+ }
179
+
180
+ case 'publish': {
181
+ const name = args[1];
182
+ const collections = flagList('collections');
183
+ if (!name || collections.length === 0) {
184
+ print('Usage: silo publish <name> --collections <id1> <id2> ...');
185
+ process.exit(1);
186
+ }
187
+ const desc = flag('description') || '';
188
+ const result = packs.bundle(name, collections, { description: desc });
189
+ print(`Published pack "${name}" (${result.claimCount} claims) -> ${result.path}`);
190
+ break;
191
+ }
192
+
193
+ case 'packs': {
194
+ const allPacks = packs.list();
195
+ if (jsonMode) {
196
+ print(JSON.stringify(allPacks));
197
+ break;
198
+ }
199
+ if (allPacks.length === 0) {
200
+ print('No packs available.');
201
+ } else {
202
+ print('Available packs:\n');
203
+ for (const p of allPacks) {
204
+ print(` ${p.id} ${p.name} (${p.claimCount} claims, v${p.version}, ${p.source})`);
205
+ if (p.description) print(` ${p.description.slice(0, 100)}`);
206
+ print('');
207
+ }
208
+ }
209
+ break;
210
+ }
211
+
212
+ case 'templates': {
213
+ const allTemplates = templates.list();
214
+ if (jsonMode) {
215
+ print(JSON.stringify(allTemplates));
216
+ break;
217
+ }
218
+ if (allTemplates.length === 0) {
219
+ print('No templates saved yet. Use the Templates API to create them.');
220
+ } else {
221
+ for (const t of allTemplates) {
222
+ print(` ${t.id} "${t.question || t.name}" (${t.seedClaims} seed claims) [${t.tags.join(', ')}]`);
223
+ }
224
+ }
225
+ break;
226
+ }
227
+
228
+ case 'install': {
229
+ const filePath = args[1];
230
+ if (!filePath) {
231
+ print('Usage: silo install <pack-file.json>');
232
+ process.exit(1);
233
+ }
234
+ const result = packs.install(filePath);
235
+ print(`Installed pack "${result.id}" (${result.claimCount} claims)`);
236
+ break;
237
+ }
238
+
239
+ case 'analyze': {
240
+ const { analyzeLibrary } = require('../lib/analytics.js');
241
+ const result = analyzeLibrary(store);
242
+ if (jsonMode) {
243
+ print(JSON.stringify(result));
244
+ break;
245
+ }
246
+ if (!result.available) {
247
+ print(`silo analyze: harvest not found.\n\nThe analyze command requires @grainulation/harvest in a sibling directory.\nExpected locations:\n ../harvest/lib/analyzer.js\n\nInstall harvest alongside silo and try again.`);
248
+ process.exit(1);
249
+ }
250
+ if (!result.analysis) {
251
+ print(`silo analyze: ${result.reason || 'no data to analyze'}`);
252
+ break;
253
+ }
254
+ print(`Cross-library analytics (${result.collectionCount} collection(s)):\n`);
255
+ const analysis = result.analysis;
256
+ if (analysis.typeDistribution) {
257
+ print('Type distribution:');
258
+ for (const [type, count] of Object.entries(analysis.typeDistribution)) {
259
+ print(` ${type}: ${count}`);
260
+ }
261
+ print('');
262
+ }
263
+ if (analysis.evidenceQuality) {
264
+ print('Evidence quality:');
265
+ for (const [tier, count] of Object.entries(analysis.evidenceQuality)) {
266
+ print(` ${tier}: ${count}`);
267
+ }
268
+ print('');
269
+ }
270
+ // Print any other top-level keys from the analysis
271
+ for (const [key, value] of Object.entries(analysis)) {
272
+ if (key === 'typeDistribution' || key === 'evidenceQuality') continue;
273
+ if (typeof value === 'object') {
274
+ print(`${key}: ${JSON.stringify(value, null, 2)}`);
275
+ } else {
276
+ print(`${key}: ${value}`);
277
+ }
278
+ }
279
+ break;
280
+ }
281
+
282
+ case 'serve-mcp': {
283
+ const serveMcp = require('../lib/serve-mcp.js');
284
+ serveMcp.run(process.cwd());
285
+ break;
286
+ }
287
+
288
+ case 'serve': {
289
+ // Dynamic import for ESM server module -- use fork() for proper stdio
290
+ const port = flag('port') || '9095';
291
+ const root = flag('root') || process.cwd();
292
+ const serverArgs = [];
293
+ if (flag('port')) { serverArgs.push('--port', port); }
294
+ if (flag('root')) { serverArgs.push('--root', root); }
295
+ const { fork } = require('node:child_process');
296
+ const path = require('node:path');
297
+ const serverPath = path.join(__dirname, '..', 'lib', 'server.js');
298
+ const child = fork(serverPath, serverArgs, {
299
+ stdio: 'inherit',
300
+ env: process.env,
301
+ });
302
+ child.on('error', (err) => {
303
+ process.stderr.write(`silo: error starting server: ${err.message}\n`);
304
+ process.exit(1);
305
+ });
306
+ child.on('exit', (code) => process.exit(code || 0));
307
+ process.on('SIGTERM', () => child.kill('SIGTERM'));
308
+ process.on('SIGINT', () => child.kill('SIGINT'));
309
+ break;
310
+ }
311
+
312
+ case 'help':
313
+ case '--help':
314
+ case '-h':
315
+ case undefined:
316
+ usage();
317
+ break;
318
+
319
+ default:
320
+ print(`silo: unknown command: ${command}\n`);
321
+ usage();
322
+ process.exit(1);
323
+ }
324
+ } catch (err) {
325
+ process.stderr.write(`silo: ${err.message}\n`);
326
+ process.exit(1);
327
+ }
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * silo -> harvest edge: cross-library analytics.
5
+ *
6
+ * Feeds silo's stored claim collections to harvest's analyzer
7
+ * to surface patterns, evidence quality trends, and type distribution
8
+ * across the entire knowledge base. Graceful fallback if harvest
9
+ * is not available.
10
+ */
11
+
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const HARVEST_SIBLINGS = [
16
+ path.join(__dirname, '..', '..', 'harvest'),
17
+ path.join(__dirname, '..', '..', '..', 'harvest'),
18
+ ];
19
+
20
+ /**
21
+ * Try to load harvest's analyzer module from a sibling checkout.
22
+ * Returns the analyze function or null.
23
+ */
24
+ function loadHarvestAnalyzer() {
25
+ for (const dir of HARVEST_SIBLINGS) {
26
+ const analyzerPath = path.join(dir, 'lib', 'analyzer.js');
27
+ if (fs.existsSync(analyzerPath)) {
28
+ try {
29
+ const mod = require(analyzerPath);
30
+ if (typeof mod.analyze === 'function') return mod.analyze;
31
+ } catch { continue; }
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Run cross-library analytics on all stored claim collections.
39
+ * @param {import('./store').Store} store — an initialized silo Store instance
40
+ * @returns {{ available: boolean, analysis?: object, collectionCount?: number }}
41
+ */
42
+ function analyzeLibrary(store) {
43
+ const analyze = loadHarvestAnalyzer();
44
+ if (!analyze) {
45
+ return { available: false, reason: 'harvest analyzer not found in sibling directories' };
46
+ }
47
+
48
+ const collections = store.list();
49
+ if (collections.length === 0) {
50
+ return { available: true, analysis: null, collectionCount: 0, reason: 'no collections stored' };
51
+ }
52
+
53
+ // Convert silo collections into the sprint format harvest expects
54
+ const sprints = [];
55
+ for (const entry of collections) {
56
+ const data = store.getClaims(entry.id);
57
+ if (!data || !data.claims) continue;
58
+ sprints.push({
59
+ name: entry.name || entry.id,
60
+ claims: data.claims,
61
+ });
62
+ }
63
+
64
+ if (sprints.length === 0) {
65
+ return { available: true, analysis: null, collectionCount: 0, reason: 'no valid claim data' };
66
+ }
67
+
68
+ const analysis = analyze(sprints);
69
+ return {
70
+ available: true,
71
+ collectionCount: sprints.length,
72
+ analysis,
73
+ };
74
+ }
75
+
76
+ module.exports = { analyzeLibrary, loadHarvestAnalyzer };