@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 +13 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/silo.js +327 -0
- package/lib/analytics.js +76 -0
- package/lib/import-export.js +174 -0
- package/lib/index.js +28 -0
- package/lib/packs.js +184 -0
- package/lib/search.js +128 -0
- package/lib/serve-mcp.js +337 -0
- package/lib/server.js +425 -0
- package/lib/store.js +145 -0
- package/lib/templates.js +139 -0
- package/package.json +48 -0
- package/packs/api-design.json +189 -0
- package/packs/architecture.json +175 -0
- package/packs/ci-cd.json +175 -0
- package/packs/compliance.json +203 -0
- package/packs/data-engineering.json +175 -0
- package/packs/frontend.json +175 -0
- package/packs/migration.json +147 -0
- package/packs/observability.json +175 -0
- package/packs/security.json +175 -0
- package/packs/team-process.json +175 -0
- package/packs/testing.json +147 -0
- package/public/grainulation-tokens.css +321 -0
- package/public/index.html +803 -0
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
|
+
}
|
package/lib/analytics.js
ADDED
|
@@ -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 };
|