@hasna/knowledge 0.2.10 → 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.
@@ -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
+ }
@@ -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 writes = [
90
- store.put({ key: schemaKey, body: agentSchemaTemplate(), content_type: 'text/markdown' }),
91
- store.put({ key: rootIndexKey, body: rootIndexTemplate(), content_type: 'text/markdown' }),
92
- store.put({ key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: 'text/markdown' }),
93
- store.put({ key: logKey, body: `${JSON.stringify(event)}\n`, content_type: 'application/x-ndjson' }),
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(writes);
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
  }
package/src/workspace.ts CHANGED
@@ -39,6 +39,25 @@ export interface KnowledgeConfig {
39
39
  preferred_ref: 'open-files';
40
40
  allowed_schemes: string[];
41
41
  };
42
+ providers?: {
43
+ default_model?: string;
44
+ aliases?: Record<string, string>;
45
+ openai?: {
46
+ api_key_env?: string;
47
+ base_url?: string;
48
+ default_model?: string;
49
+ };
50
+ anthropic?: {
51
+ api_key_env?: string;
52
+ base_url?: string;
53
+ default_model?: string;
54
+ };
55
+ deepseek?: {
56
+ api_key_env?: string;
57
+ base_url?: string;
58
+ default_model?: string;
59
+ };
60
+ };
42
61
  safety?: {
43
62
  network?: {
44
63
  web_search_enabled?: boolean;
@@ -95,6 +114,28 @@ export function defaultKnowledgeConfig(): KnowledgeConfig {
95
114
  preferred_ref: 'open-files',
96
115
  allowed_schemes: ['open-files', 's3', 'file', 'https', 'http'],
97
116
  },
117
+ providers: {
118
+ default_model: 'openai:gpt-5.2',
119
+ aliases: {
120
+ fast: 'openai:gpt-5-mini',
121
+ reasoning: 'anthropic:claude-opus-4-6',
122
+ sonnet: 'anthropic:claude-sonnet-4-6',
123
+ deepseek: 'deepseek:deepseek-chat',
124
+ 'deepseek-reasoning': 'deepseek:deepseek-reasoner',
125
+ },
126
+ openai: {
127
+ api_key_env: 'OPENAI_API_KEY',
128
+ default_model: 'gpt-5.2',
129
+ },
130
+ anthropic: {
131
+ api_key_env: 'ANTHROPIC_API_KEY',
132
+ default_model: 'claude-sonnet-4-6',
133
+ },
134
+ deepseek: {
135
+ api_key_env: 'DEEPSEEK_API_KEY',
136
+ default_model: 'deepseek-chat',
137
+ },
138
+ },
98
139
  safety: {
99
140
  network: {
100
141
  web_search_enabled: false,