@hasna/knowledge 0.2.11 → 0.2.12
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/README.md +18 -2
- package/bin/open-knowledge-mcp.js +218 -10
- package/bin/open-knowledge.js +44 -31
- package/docs/architecture/ai-native-knowledge-base.md +12 -0
- package/package.json +1 -1
- package/src/cli.ts +31 -4
- package/src/knowledge-db.ts +2 -0
- package/src/mcp.js +12 -0
- package/src/service.ts +26 -2
- package/src/storage-contract.ts +265 -0
- package/src/wiki-layout.ts +22 -6
package/src/cli.ts
CHANGED
|
@@ -60,7 +60,7 @@ interface ParseResult {
|
|
|
60
60
|
flags: Flags;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'db', 'wiki', 'source', 'ingest', 'reindex', 'providers', 'safety', 'help'];
|
|
63
|
+
const COMMANDS = ['add', 'list', 'get', 'delete', 'update', 'archive', 'restore', 'upsert', 'untag', 'export', 'prune', 'dedupe', 'stats', 'paths', 'storage', 'db', 'wiki', 'source', 'ingest', 'reindex', 'providers', 'safety', 'help'];
|
|
64
64
|
const COMMAND_ALIASES: Record<string, string> = {
|
|
65
65
|
ls: 'list',
|
|
66
66
|
rm: 'delete',
|
|
@@ -162,6 +162,7 @@ Commands:
|
|
|
162
162
|
dedupe Remove duplicate items by title+content (requires --yes)
|
|
163
163
|
stats Show knowledge base statistics
|
|
164
164
|
paths Show resolved workspace/store paths
|
|
165
|
+
storage status|validate Inspect local/S3 artifact storage contract
|
|
165
166
|
db init|stats Initialize or inspect local knowledge.db
|
|
166
167
|
wiki init Initialize scalable wiki/schema/index/log artifacts
|
|
167
168
|
source resolve <source-ref> Resolve read-only source content and citation evidence
|
|
@@ -230,6 +231,7 @@ function printCommandHelp(command: string): void {
|
|
|
230
231
|
if (command === 'dedupe') { console.log('Usage: open-knowledge dedupe --yes [--json]'); return; }
|
|
231
232
|
if (command === 'stats') { console.log('Usage: open-knowledge stats [--json]'); return; }
|
|
232
233
|
if (command === 'paths') { console.log('Usage: open-knowledge paths [--scope local|global|project] [--json]'); return; }
|
|
234
|
+
if (command === 'storage') { console.log('Usage: open-knowledge storage status|validate [--scope local|global|project] [--json]'); return; }
|
|
233
235
|
if (command === 'db') { console.log('Usage: open-knowledge db init|stats [--scope local|global|project] [--json]'); return; }
|
|
234
236
|
if (command === 'wiki') { console.log('Usage: open-knowledge wiki init [--scope local|global|project] [--json]'); return; }
|
|
235
237
|
if (command === 'source') { console.log('Usage: open-knowledge source resolve <source-ref> [--purpose knowledge_answer|knowledge_index] [--limit <n>] [--scope local|global|project] [--json]'); return; }
|
|
@@ -281,11 +283,11 @@ async function run(argv: string[]): Promise<void> {
|
|
|
281
283
|
if (flags.completions) {
|
|
282
284
|
const shell = flags.completions;
|
|
283
285
|
if (shell === 'bash') {
|
|
284
|
-
console.log(`_open_knowledge() { local cur; cur="${"$"}{COMP_WORDS[COMP_CWORD]}"; COMPREPLY=($(compgen -W "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive --json --yes --help --version --desc --page --limit --search --sort --id --store --title --content --url --tag --format --completions --purpose --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
|
|
286
|
+
console.log(`_open_knowledge() { local cur; cur="${"$"}{COMP_WORDS[COMP_CWORD]}"; COMPREPLY=($(compgen -W "add list get update archive restore upsert untag delete export prune dedupe stats paths storage db wiki source ingest reindex providers safety help ls rm edit unarchive --json --yes --help --version --desc --page --limit --search --sort --id --store --title --content --url --tag --format --completions --purpose --no-color --scope --archived --include-archived" -- "$cur")); }; complete -F _open_knowledge open-knowledge`);
|
|
285
287
|
} else if (shell === 'zsh') {
|
|
286
|
-
console.log(`#compdef open-knowledge\n_open_knowledge() { _arguments -C "1: :(add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(-p --page)"{-p,--page}"[page number]:number:" "(-l --limit)"{-l,--limit}"[items per page]:number:" "(-s --search)"{-s,--search}"[search text]:text:" "(--sort)--sort"\{created,title\}:" "(--id)--id[item id]:id:" "(--store)--store[store path]:path:" "(--title)--title[new title]:" "(--content)--content[new content]:" "(--url)--url[source url]:" "(-t --tag)"{-t,--tag}"[tag]:tag:" "(--format)--format[json|jsonl]:" "(--completions)--completions[output completions]:shell:(bash zsh fish):" "(--purpose)--purpose[purpose]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
|
|
288
|
+
console.log(`#compdef open-knowledge\n_open_knowledge() { _arguments -C "1: :(add list get update archive restore upsert untag delete export prune dedupe stats paths storage db wiki source ingest reindex providers safety help ls rm edit unarchive)" "(--json)--json" "(--yes)-y" "(--help)--help" "(--version)--version" "(--desc)--desc" "(--archived)--archived" "(--include-archived)--include-archived" "(-p --page)"{-p,--page}"[page number]:number:" "(-l --limit)"{-l,--limit}"[items per page]:number:" "(-s --search)"{-s,--search}"[search text]:text:" "(--sort)--sort"\{created,title\}:" "(--id)--id[item id]:id:" "(--store)--store[store path]:path:" "(--title)--title[new title]:" "(--content)--content[new content]:" "(--url)--url[source url]:" "(-t --tag)"{-t,--tag}"[tag]:tag:" "(--format)--format[json|jsonl]:" "(--completions)--completions[output completions]:shell:(bash zsh fish):" "(--purpose)--purpose[purpose]:" "(--no-color)--no-color[disable color]" "(--scope)--scope"\{local,global,project\}:" }; _open_knowledge`);
|
|
287
289
|
} else if (shell === 'fish') {
|
|
288
|
-
console.log(`complete -c open-knowledge -f; complete -c open-knowledge -a "add list get update archive restore upsert untag delete export prune dedupe stats paths db wiki source ingest reindex providers safety help ls rm edit unarchive"; complete -c open-knowledge -l json; complete -c open-knowledge -l yes -s y; complete -c open-knowledge -l help -s h; complete -c open-knowledge -l version -s v; complete -c open-knowledge -l desc; complete -c open-knowledge -l archived; complete -c open-knowledge -l include-archived; complete -c open-knowledge -s p -l page; complete -c open-knowledge -s l -l limit; complete -c open-knowledge -s s -l search; complete -c open-knowledge -l sort; complete -c open-knowledge -l id; complete -c open-knowledge -l store; complete -c open-knowledge -l title; complete -c open-knowledge -l content; complete -c open-knowledge -l url; complete -c open-knowledge -s t -l tag; complete -c open-knowledge -l format; complete -c open-knowledge -l completions; complete -c open-knowledge -l purpose; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
|
|
290
|
+
console.log(`complete -c open-knowledge -f; complete -c open-knowledge -a "add list get update archive restore upsert untag delete export prune dedupe stats paths storage db wiki source ingest reindex providers safety help ls rm edit unarchive"; complete -c open-knowledge -l json; complete -c open-knowledge -l yes -s y; complete -c open-knowledge -l help -s h; complete -c open-knowledge -l version -s v; complete -c open-knowledge -l desc; complete -c open-knowledge -l archived; complete -c open-knowledge -l include-archived; complete -c open-knowledge -s p -l page; complete -c open-knowledge -s l -l limit; complete -c open-knowledge -s s -l search; complete -c open-knowledge -l sort; complete -c open-knowledge -l id; complete -c open-knowledge -l store; complete -c open-knowledge -l title; complete -c open-knowledge -l content; complete -c open-knowledge -l url; complete -c open-knowledge -s t -l tag; complete -c open-knowledge -l format; complete -c open-knowledge -l completions; complete -c open-knowledge -l purpose; complete -c open-knowledge -l no-color; complete -c open-knowledge -l scope -a "local global project"`);
|
|
289
291
|
} else {
|
|
290
292
|
throw new Error("Invalid --completions value. Use 'bash', 'zsh', or 'fish'.");
|
|
291
293
|
}
|
|
@@ -311,6 +313,31 @@ async function run(argv: string[]): Promise<void> {
|
|
|
311
313
|
return;
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
if (command === 'storage') {
|
|
317
|
+
const action = positional[1] ?? 'status';
|
|
318
|
+
if (action === 'status') {
|
|
319
|
+
const contract = service.storageContract();
|
|
320
|
+
const validation = service.validateStorage();
|
|
321
|
+
output({
|
|
322
|
+
ok: validation.ok,
|
|
323
|
+
...contract,
|
|
324
|
+
validation,
|
|
325
|
+
message: `${contract.storage_type} artifact storage at ${contract.artifact_store.uri_prefix}`,
|
|
326
|
+
}, flags.json);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (action === 'validate') {
|
|
330
|
+
const validation = service.validateStorage();
|
|
331
|
+
output({
|
|
332
|
+
ok: validation.ok,
|
|
333
|
+
validation,
|
|
334
|
+
message: validation.ok ? 'Storage contract valid' : `Storage contract invalid: ${validation.errors.join('; ')}`,
|
|
335
|
+
}, flags.json);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
throw new Error("Invalid storage action. Use 'status' or 'validate'.");
|
|
339
|
+
}
|
|
340
|
+
|
|
314
341
|
if (command === 'db') {
|
|
315
342
|
const action = positional[1] ?? 'init';
|
|
316
343
|
if (action !== 'init' && action !== 'stats') {
|
package/src/knowledge-db.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface KnowledgeDbStats {
|
|
|
16
16
|
redaction_findings: number;
|
|
17
17
|
audit_events: number;
|
|
18
18
|
approval_gates: number;
|
|
19
|
+
storage_objects: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const MIGRATION_1 = `
|
|
@@ -280,6 +281,7 @@ export function getKnowledgeDbStats(path: string): KnowledgeDbStats {
|
|
|
280
281
|
redaction_findings: count(db, 'redaction_findings'),
|
|
281
282
|
audit_events: count(db, 'audit_events'),
|
|
282
283
|
approval_gates: count(db, 'approval_gates'),
|
|
284
|
+
storage_objects: count(db, 'storage_objects'),
|
|
283
285
|
};
|
|
284
286
|
} finally {
|
|
285
287
|
db.close();
|
package/src/mcp.js
CHANGED
|
@@ -77,6 +77,18 @@ export function buildServer() {
|
|
|
77
77
|
return jsonText(createKnowledgeService({ scope }).paths());
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
registerTool(server, 'ok_storage_status', 'Knowledge storage status', 'Inspect local/S3 artifact storage, source ownership, and scalability contract', {
|
|
81
|
+
scope: scopeField,
|
|
82
|
+
}, async ({ scope }) => {
|
|
83
|
+
const service = createKnowledgeService({ scope });
|
|
84
|
+
const validation = service.validateStorage();
|
|
85
|
+
return jsonText({
|
|
86
|
+
ok: validation.ok,
|
|
87
|
+
...service.storageContract(),
|
|
88
|
+
validation,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
80
92
|
registerTool(server, 'ok_parse_source_ref', 'Parse source reference', 'Parse and validate an open-files, S3, file, or web source ref', {
|
|
81
93
|
uri: z.string().describe('Source reference URI'),
|
|
82
94
|
}, async ({ uri }) => {
|
package/src/service.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { createArtifactStore } from './artifact-store';
|
|
2
2
|
import { consumeOpenFilesOutbox } from './outbox-consume';
|
|
3
|
-
import { getKnowledgeDbStats, migrateKnowledgeDb } from './knowledge-db';
|
|
3
|
+
import { getKnowledgeDbStats, migrateKnowledgeDb, openKnowledgeDb } from './knowledge-db';
|
|
4
4
|
import { ingestOpenFilesManifest } from './manifest-ingest';
|
|
5
5
|
import { ingestSourceRef } from './source-ingest';
|
|
6
6
|
import { resolveOpenFilesSource } from './source-resolver';
|
|
7
7
|
import { providerStatus, listModelRegistry, type ProviderStatusResult, type ModelRegistryEntry } from './providers';
|
|
8
8
|
import { resolveSafetyPolicy } from './safety';
|
|
9
|
+
import {
|
|
10
|
+
recordStorageObjects,
|
|
11
|
+
resolveStorageContract,
|
|
12
|
+
validateStorageConfig,
|
|
13
|
+
type StorageContract,
|
|
14
|
+
type StorageValidationResult,
|
|
15
|
+
} from './storage-contract';
|
|
9
16
|
import { initializeWikiLayout } from './wiki-layout';
|
|
10
17
|
import {
|
|
11
18
|
ensureKnowledgeWorkspace,
|
|
@@ -76,6 +83,14 @@ export class KnowledgeService {
|
|
|
76
83
|
return createArtifactStore(this.config(), this.ensureWorkspace());
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
storageContract(): StorageContract {
|
|
87
|
+
return resolveStorageContract(this.config(), this.ensureWorkspace(), this.scope);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
validateStorage(): StorageValidationResult {
|
|
91
|
+
return validateStorageConfig(this.config(), this.ensureWorkspace());
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
paths(): KnowledgePathsResult {
|
|
80
95
|
const workspace = this.ensureWorkspace();
|
|
81
96
|
return {
|
|
@@ -107,7 +122,16 @@ export class KnowledgeService {
|
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
async initWiki() {
|
|
110
|
-
|
|
125
|
+
const workspace = this.ensureWorkspace();
|
|
126
|
+
migrateKnowledgeDb(workspace.knowledgeDbPath);
|
|
127
|
+
const result = await initializeWikiLayout(this.artifactStore());
|
|
128
|
+
const db = openKnowledgeDb(workspace.knowledgeDbPath);
|
|
129
|
+
try {
|
|
130
|
+
recordStorageObjects(db, result.artifacts);
|
|
131
|
+
} finally {
|
|
132
|
+
db.close();
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
111
135
|
}
|
|
112
136
|
|
|
113
137
|
async ingestManifest(input: string) {
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import type { Database } from 'bun:sqlite';
|
|
3
|
+
import type { KnowledgeConfig, KnowledgeWorkspace } from './workspace';
|
|
4
|
+
import { HASNA_KNOWLEDGE_APP_PATH } from './workspace';
|
|
5
|
+
|
|
6
|
+
export interface StorageArtifactClass {
|
|
7
|
+
kind: string;
|
|
8
|
+
prefix: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface StorageContract {
|
|
13
|
+
scope: string;
|
|
14
|
+
mode: KnowledgeConfig['mode'];
|
|
15
|
+
storage_type: KnowledgeConfig['storage']['type'];
|
|
16
|
+
workspace_home: string;
|
|
17
|
+
local_layout: {
|
|
18
|
+
app_path: string;
|
|
19
|
+
config_path: string;
|
|
20
|
+
json_store_path: string;
|
|
21
|
+
knowledge_db_path: string;
|
|
22
|
+
directories: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
artifact_store: {
|
|
25
|
+
type: KnowledgeConfig['storage']['type'];
|
|
26
|
+
artifacts_root: string;
|
|
27
|
+
uri_prefix: string;
|
|
28
|
+
s3: {
|
|
29
|
+
bucket: string;
|
|
30
|
+
prefix: string;
|
|
31
|
+
region: string | null;
|
|
32
|
+
profile: string | null;
|
|
33
|
+
server_side_encryption: string | null;
|
|
34
|
+
kms_key_configured: boolean;
|
|
35
|
+
} | null;
|
|
36
|
+
};
|
|
37
|
+
source_ownership: {
|
|
38
|
+
owner: 'open-files';
|
|
39
|
+
preferred_ref: string;
|
|
40
|
+
allowed_schemes: string[];
|
|
41
|
+
raw_source_bytes_stored_in_open_knowledge: false;
|
|
42
|
+
stores: string[];
|
|
43
|
+
does_not_store: string[];
|
|
44
|
+
};
|
|
45
|
+
generated_artifacts: StorageArtifactClass[];
|
|
46
|
+
scalability: {
|
|
47
|
+
catalog: string;
|
|
48
|
+
indexes: string;
|
|
49
|
+
logs: string;
|
|
50
|
+
markdown: string;
|
|
51
|
+
};
|
|
52
|
+
warnings: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StorageValidationResult {
|
|
56
|
+
ok: boolean;
|
|
57
|
+
errors: string[];
|
|
58
|
+
warnings: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface GeneratedStorageObject {
|
|
62
|
+
uri: string;
|
|
63
|
+
key: string;
|
|
64
|
+
kind: string;
|
|
65
|
+
content_type?: string;
|
|
66
|
+
hash?: string;
|
|
67
|
+
size_bytes?: number;
|
|
68
|
+
metadata?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const GENERATED_ARTIFACTS: StorageArtifactClass[] = [
|
|
72
|
+
{
|
|
73
|
+
kind: 'schema',
|
|
74
|
+
prefix: 'schemas/',
|
|
75
|
+
description: 'Machine-readable agent schemas and source rules.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: 'index',
|
|
79
|
+
prefix: 'indexes/',
|
|
80
|
+
description: 'Small orientation indexes and future shard manifests.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
kind: 'log',
|
|
84
|
+
prefix: 'logs/',
|
|
85
|
+
description: 'Append-only JSONL run and wiki-maintenance log partitions.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
kind: 'run',
|
|
89
|
+
prefix: 'runs/',
|
|
90
|
+
description: 'Prompt/tool/cost ledgers and generated output records.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
kind: 'wiki_page',
|
|
94
|
+
prefix: 'wiki/',
|
|
95
|
+
description: 'Generated cited Markdown pages, not raw source files.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
kind: 'export',
|
|
99
|
+
prefix: 'exports/',
|
|
100
|
+
description: 'Portable exports and snapshots of derived knowledge state.',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
export function hashArtifactBody(body: string | Uint8Array): { hash: string; size_bytes: number } {
|
|
105
|
+
const bytes = typeof body === 'string' ? Buffer.from(body) : Buffer.from(body);
|
|
106
|
+
return {
|
|
107
|
+
hash: `sha256:${createHash('sha256').update(bytes).digest('hex')}`,
|
|
108
|
+
size_bytes: bytes.byteLength,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function artifactKindForKey(key: string): string {
|
|
113
|
+
const match = GENERATED_ARTIFACTS.find((entry) => key.startsWith(entry.prefix));
|
|
114
|
+
return match?.kind ?? 'artifact';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveStorageContract(
|
|
118
|
+
config: KnowledgeConfig,
|
|
119
|
+
workspace: KnowledgeWorkspace,
|
|
120
|
+
scope = 'global',
|
|
121
|
+
): StorageContract {
|
|
122
|
+
const validation = validateStorageConfig(config, workspace);
|
|
123
|
+
const s3 = config.storage.s3 ?? null;
|
|
124
|
+
const prefix = s3?.prefix?.replace(/^\/+|\/+$/g, '') ?? '';
|
|
125
|
+
const s3UriPrefix = s3 ? `s3://${s3.bucket}/${prefix ? `${prefix}/` : ''}` : '';
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
scope,
|
|
129
|
+
mode: config.mode,
|
|
130
|
+
storage_type: config.storage.type,
|
|
131
|
+
workspace_home: workspace.home,
|
|
132
|
+
local_layout: {
|
|
133
|
+
app_path: HASNA_KNOWLEDGE_APP_PATH,
|
|
134
|
+
config_path: workspace.configPath,
|
|
135
|
+
json_store_path: workspace.jsonStorePath,
|
|
136
|
+
knowledge_db_path: workspace.knowledgeDbPath,
|
|
137
|
+
directories: {
|
|
138
|
+
artifacts: workspace.artifactsDir,
|
|
139
|
+
cache: workspace.cacheDir,
|
|
140
|
+
exports: workspace.exportsDir,
|
|
141
|
+
indexes: workspace.indexesDir,
|
|
142
|
+
logs: workspace.logsDir,
|
|
143
|
+
runs: workspace.runsDir,
|
|
144
|
+
schemas: workspace.schemasDir,
|
|
145
|
+
wiki: workspace.wikiDir,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
artifact_store: {
|
|
149
|
+
type: config.storage.type,
|
|
150
|
+
artifacts_root: config.storage.artifacts_root,
|
|
151
|
+
uri_prefix: config.storage.type === 's3' ? s3UriPrefix : `file://${workspace.artifactsDir}/`,
|
|
152
|
+
s3: s3
|
|
153
|
+
? {
|
|
154
|
+
bucket: s3.bucket,
|
|
155
|
+
prefix,
|
|
156
|
+
region: s3.region ?? null,
|
|
157
|
+
profile: s3.profile ?? null,
|
|
158
|
+
server_side_encryption: s3.server_side_encryption ?? null,
|
|
159
|
+
kms_key_configured: Boolean(s3.kms_key_id),
|
|
160
|
+
}
|
|
161
|
+
: null,
|
|
162
|
+
},
|
|
163
|
+
source_ownership: {
|
|
164
|
+
owner: 'open-files',
|
|
165
|
+
preferred_ref: config.sources.preferred_ref,
|
|
166
|
+
allowed_schemes: config.sources.allowed_schemes,
|
|
167
|
+
raw_source_bytes_stored_in_open_knowledge: false,
|
|
168
|
+
stores: [
|
|
169
|
+
'source refs',
|
|
170
|
+
'source revisions and hashes',
|
|
171
|
+
'citation spans',
|
|
172
|
+
'redacted extracted chunks',
|
|
173
|
+
'embeddings',
|
|
174
|
+
'generated wiki artifacts',
|
|
175
|
+
'indexes',
|
|
176
|
+
'run ledgers',
|
|
177
|
+
],
|
|
178
|
+
does_not_store: [
|
|
179
|
+
'raw open-files bytes',
|
|
180
|
+
'S3 object credentials',
|
|
181
|
+
'connector secrets',
|
|
182
|
+
'hosted tenant ownership state',
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
generated_artifacts: GENERATED_ARTIFACTS,
|
|
186
|
+
scalability: {
|
|
187
|
+
catalog: 'knowledge.db tracks sources, revisions, chunks, citations, indexes, runs, and storage_objects.',
|
|
188
|
+
indexes: 'Indexes are cataloged DB rows plus sharded artifacts, not one giant index.md.',
|
|
189
|
+
logs: 'Logs use dated JSONL partitions under logs/yyyy/mm/dd.jsonl.',
|
|
190
|
+
markdown: 'Markdown pages are the readable wiki layer over DB/object-store state.',
|
|
191
|
+
},
|
|
192
|
+
warnings: validation.warnings,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function validateStorageConfig(config: KnowledgeConfig, workspace: KnowledgeWorkspace): StorageValidationResult {
|
|
197
|
+
const errors: string[] = [];
|
|
198
|
+
const warnings: string[] = [];
|
|
199
|
+
|
|
200
|
+
if (!workspace.home.endsWith(HASNA_KNOWLEDGE_APP_PATH)) {
|
|
201
|
+
warnings.push(`Workspace home does not end with ${HASNA_KNOWLEDGE_APP_PATH}: ${workspace.home}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (config.storage.type === 's3') {
|
|
205
|
+
if (!config.storage.s3?.bucket) errors.push('storage.s3.bucket is required when storage.type is s3.');
|
|
206
|
+
if (!config.storage.s3?.prefix) warnings.push('storage.s3.prefix is empty; generated knowledge artifacts will be written at the bucket root.');
|
|
207
|
+
if (config.mode === 'local') warnings.push('storage.type is s3 while mode is local; this is valid for BYO S3, but hosted wrappers should set mode to hosted.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (config.storage.type === 'local' && config.storage.s3) {
|
|
211
|
+
warnings.push('storage.s3 is configured but ignored while storage.type is local.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (config.sources.preferred_ref !== 'open-files') {
|
|
215
|
+
warnings.push('sources.preferred_ref should stay open-files for durable company knowledge.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!config.sources.allowed_schemes.includes('open-files')) {
|
|
219
|
+
errors.push('sources.allowed_schemes must include open-files.');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
ok: errors.length === 0,
|
|
224
|
+
errors,
|
|
225
|
+
warnings,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function recordStorageObjects(db: Database, objects: GeneratedStorageObject[], now = new Date()): void {
|
|
230
|
+
const timestamp = now.toISOString();
|
|
231
|
+
const statement = db.prepare(`
|
|
232
|
+
INSERT INTO storage_objects (
|
|
233
|
+
id, artifact_uri, kind, content_type, hash, size_bytes, metadata_json, created_at, updated_at
|
|
234
|
+
)
|
|
235
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
236
|
+
ON CONFLICT(artifact_uri) DO UPDATE SET
|
|
237
|
+
kind = excluded.kind,
|
|
238
|
+
content_type = excluded.content_type,
|
|
239
|
+
hash = excluded.hash,
|
|
240
|
+
size_bytes = excluded.size_bytes,
|
|
241
|
+
metadata_json = excluded.metadata_json,
|
|
242
|
+
updated_at = excluded.updated_at
|
|
243
|
+
`);
|
|
244
|
+
|
|
245
|
+
const insert = db.transaction((entries: GeneratedStorageObject[]) => {
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
statement.run(
|
|
248
|
+
randomUUID(),
|
|
249
|
+
entry.uri,
|
|
250
|
+
entry.kind,
|
|
251
|
+
entry.content_type ?? null,
|
|
252
|
+
entry.hash ?? null,
|
|
253
|
+
entry.size_bytes ?? null,
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
key: entry.key,
|
|
256
|
+
...(entry.metadata ?? {}),
|
|
257
|
+
}),
|
|
258
|
+
timestamp,
|
|
259
|
+
timestamp,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
insert(objects);
|
|
265
|
+
}
|
package/src/wiki-layout.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ArtifactStore } from './artifact-store';
|
|
2
|
+
import {
|
|
3
|
+
artifactKindForKey,
|
|
4
|
+
hashArtifactBody,
|
|
5
|
+
type GeneratedStorageObject,
|
|
6
|
+
} from './storage-contract';
|
|
2
7
|
|
|
3
8
|
export interface WikiLayoutInitResult {
|
|
4
9
|
schema_key: string;
|
|
5
10
|
root_index_key: string;
|
|
6
11
|
wiki_readme_key: string;
|
|
7
12
|
log_key: string;
|
|
13
|
+
artifacts: GeneratedStorageObject[];
|
|
8
14
|
written: string[];
|
|
9
15
|
}
|
|
10
16
|
|
|
@@ -86,19 +92,29 @@ export async function initializeWikiLayout(store: ArtifactStore, now = new Date(
|
|
|
86
92
|
wiki_readme_key: wikiReadmeKey,
|
|
87
93
|
};
|
|
88
94
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
const entries = [
|
|
96
|
+
{ key: schemaKey, body: agentSchemaTemplate(), content_type: 'text/markdown' },
|
|
97
|
+
{ key: rootIndexKey, body: rootIndexTemplate(), content_type: 'text/markdown' },
|
|
98
|
+
{ key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: 'text/markdown' },
|
|
99
|
+
{ key: logKey, body: `${JSON.stringify(event)}\n`, content_type: 'application/x-ndjson' },
|
|
94
100
|
];
|
|
95
101
|
|
|
96
|
-
await Promise.all(
|
|
102
|
+
const artifacts = await Promise.all(entries.map(async (entry) => {
|
|
103
|
+
const result = await store.put(entry);
|
|
104
|
+
return {
|
|
105
|
+
key: result.key,
|
|
106
|
+
uri: result.uri,
|
|
107
|
+
kind: artifactKindForKey(entry.key),
|
|
108
|
+
content_type: entry.content_type,
|
|
109
|
+
...hashArtifactBody(entry.body),
|
|
110
|
+
};
|
|
111
|
+
}));
|
|
97
112
|
return {
|
|
98
113
|
schema_key: schemaKey,
|
|
99
114
|
root_index_key: rootIndexKey,
|
|
100
115
|
wiki_readme_key: wikiReadmeKey,
|
|
101
116
|
log_key: logKey,
|
|
117
|
+
artifacts,
|
|
102
118
|
written: [schemaKey, rootIndexKey, wikiReadmeKey, logKey],
|
|
103
119
|
};
|
|
104
120
|
}
|