@hasna/knowledge 0.2.11 → 0.2.13

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 CHANGED
@@ -62,6 +62,9 @@ open-knowledge export --format jsonl
62
62
  # Show resolved workspace paths
63
63
  open-knowledge paths --scope project --json
64
64
 
65
+ # Inspect local/S3 artifact storage and source ownership
66
+ open-knowledge storage status --scope project --json
67
+
65
68
  # Initialize the project SQLite catalog
66
69
  open-knowledge db init --scope project
67
70
 
@@ -168,6 +171,18 @@ open-knowledge paths [--scope global|project|local] [--json]
168
171
  Show the resolved Hasna app workspace, JSON compatibility store, SQLite path,
169
172
  artifact directories, and config.
170
173
 
174
+ ### storage
175
+ ```bash
176
+ open-knowledge storage status [--scope project] [--json]
177
+ open-knowledge storage validate [--scope project] [--json]
178
+ ```
179
+ Show the storage contract for local or S3-backed generated artifacts. Local mode
180
+ uses `.hasna/apps/knowledge` for config, SQLite, indexes, wiki artifacts, logs,
181
+ runs, and exports. S3 mode stores generated artifacts under the configured
182
+ knowledge bucket/prefix while `open-files` remains the source of truth for raw
183
+ source bytes. The command also reports artifact classes, allowed source ref
184
+ schemes, and warnings for non-scalable or unsafe config.
185
+
171
186
  ### db
172
187
  ```bash
173
188
  open-knowledge db init [--scope project]
@@ -277,8 +292,9 @@ open-knowledge-mcp
277
292
  The MCP server exposes item tools (`ok_add`, `ok_list`, `ok_get`, `ok_update`,
278
293
  `ok_delete`, `ok_archive`, `ok_restore`, `ok_upsert`, `ok_untag`,
279
294
  `ok_bulk_delete`, `ok_prune`, `ok_dedupe`, `ok_stats`, `ok_export`,
280
- `ok_import`, `ok_batch`), workspace inspection (`ok_paths`), and source-ref
281
- parsing/resolution (`ok_parse_source_ref`, `ok_resolve_source`).
295
+ `ok_import`, `ok_batch`), workspace/storage inspection (`ok_paths`,
296
+ `ok_storage_status`), and source-ref parsing/resolution
297
+ (`ok_parse_source_ref`, `ok_resolve_source`).
282
298
 
283
299
  ## Source And Artifact Boundary
284
300
 
@@ -298,6 +314,12 @@ source ref. It does not copy raw files into the knowledge workspace; local file,
298
314
  S3, web, and open-files inputs are converted into redacted chunks with offsets,
299
315
  hashes, revision metadata, and FTS rows.
300
316
 
317
+ Chunks, resolver results, generated wiki pages, and index records carry
318
+ provenance metadata: source owner, source ref/URI, revision/hash, chunk offsets,
319
+ read-only status, citation requirements, and stale-source status. This keeps
320
+ future semantic search and wiki compile flows tied back to `open-files` instead
321
+ of detached Markdown.
322
+
301
323
  AI provider configuration is local/BYOK by default. `open-knowledge` declares
302
324
  AI SDK v6 provider support through `ai`, `@ai-sdk/openai`,
303
325
  `@ai-sdk/anthropic`, and `@ai-sdk/deepseek`, but does not call providers until a
@@ -13660,7 +13660,7 @@ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync
13660
13660
  // package.json
13661
13661
  var package_default = {
13662
13662
  name: "@hasna/knowledge",
13663
- version: "0.2.11",
13663
+ version: "0.2.13",
13664
13664
  description: "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
13665
13665
  type: "module",
13666
13666
  bin: {
@@ -14353,6 +14353,7 @@ function openKnowledgeDb(path) {
14353
14353
  ensureParentDir(path);
14354
14354
  const db = new Database(path);
14355
14355
  db.exec("PRAGMA foreign_keys = ON;");
14356
+ db.exec("PRAGMA busy_timeout = 5000;");
14356
14357
  return db;
14357
14358
  }
14358
14359
  function migrateKnowledgeDb(path) {
@@ -14391,7 +14392,8 @@ function getKnowledgeDbStats(path) {
14391
14392
  run_events: count(db, "run_events"),
14392
14393
  redaction_findings: count(db, "redaction_findings"),
14393
14394
  audit_events: count(db, "audit_events"),
14394
- approval_gates: count(db, "approval_gates")
14395
+ approval_gates: count(db, "approval_gates"),
14396
+ storage_objects: count(db, "storage_objects")
14395
14397
  };
14396
14398
  } finally {
14397
14399
  db.close();
@@ -14898,6 +14900,50 @@ async function consumeOpenFilesOutbox(options) {
14898
14900
  import { createHash as createHash3 } from "crypto";
14899
14901
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
14900
14902
  import { basename as basename2 } from "path";
14903
+
14904
+ // src/provenance.ts
14905
+ function isStaleStatus(status) {
14906
+ return ["deleted", "stale", "invalidated", "reindex_required"].includes((status ?? "").toLowerCase());
14907
+ }
14908
+ function sourceProvenance(input) {
14909
+ const status = input.status ?? null;
14910
+ return {
14911
+ source_owner: "open-files",
14912
+ source_ref: input.source_ref ?? null,
14913
+ source_uri: input.source_uri ?? null,
14914
+ source_kind: input.source_kind ?? null,
14915
+ source_revision_id: input.source_revision_id ?? null,
14916
+ revision: input.revision ?? null,
14917
+ hash: input.hash ?? null,
14918
+ chunk_id: input.chunk_id ?? null,
14919
+ start_offset: input.start_offset ?? null,
14920
+ end_offset: input.end_offset ?? null,
14921
+ status,
14922
+ read_only: true,
14923
+ citation_required: true,
14924
+ resolver: input.resolver ?? null,
14925
+ stale: isStaleStatus(status)
14926
+ };
14927
+ }
14928
+ function generatedArtifactProvenance(input) {
14929
+ return {
14930
+ source_owner: "open-files",
14931
+ generated_from: input.generated_from,
14932
+ artifact_key: input.artifact_key,
14933
+ source_refs: input.source_refs ?? [],
14934
+ read_only_sources: true,
14935
+ citation_required: input.citation_required ?? true,
14936
+ raw_source_bytes_stored_in_open_knowledge: false
14937
+ };
14938
+ }
14939
+ function withProvenance(metadata, provenance) {
14940
+ return {
14941
+ ...metadata,
14942
+ provenance
14943
+ };
14944
+ }
14945
+
14946
+ // src/manifest-ingest.ts
14901
14947
  function stableId2(prefix, value) {
14902
14948
  return `${prefix}_${createHash3("sha256").update(value).digest("hex").slice(0, 20)}`;
14903
14949
  }
@@ -15186,15 +15232,31 @@ function insertChunks(db, sourceRevisionId, item, now, maxChars, overlapChars, s
15186
15232
  const chunks = chunkText(redacted.text, maxChars, overlapChars);
15187
15233
  for (const chunk of chunks) {
15188
15234
  const chunkId = stableId2("chk", `${sourceRevisionId}\x00${chunk.ordinal}\x00${chunk.text}`);
15189
- const metadata = {
15235
+ const provenance = sourceProvenance({
15190
15236
  source_ref: item.sourceRef,
15191
15237
  source_uri: item.sourceUri,
15238
+ source_kind: item.kind,
15239
+ source_revision_id: sourceRevisionId,
15240
+ revision: item.revision,
15241
+ hash: item.hash,
15242
+ chunk_id: chunkId,
15243
+ start_offset: chunk.startOffset,
15244
+ end_offset: chunk.endOffset,
15245
+ status: item.status,
15246
+ resolver: "open-files-read-only"
15247
+ });
15248
+ const metadata = withProvenance({
15249
+ source_ref: item.sourceRef,
15250
+ source_uri: item.sourceUri,
15251
+ source_kind: item.kind,
15252
+ source_revision_id: sourceRevisionId,
15253
+ revision: item.revision,
15192
15254
  hash: item.hash,
15193
15255
  status: item.status,
15194
15256
  path: asString2(item.raw.path) ?? null,
15195
15257
  mime: asString2(item.raw.mime) ?? asString2(item.raw.content_type) ?? null,
15196
15258
  size: asNumber(item.raw.size) ?? null
15197
- };
15259
+ }, provenance);
15198
15260
  db.run(`INSERT INTO chunks (id, source_revision_id, kind, ordinal, text, token_count, start_offset, end_offset, metadata_json, created_at)
15199
15261
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
15200
15262
  chunkId,
@@ -15487,6 +15549,19 @@ async function resolveOpenFilesSource(options) {
15487
15549
  end_offset: row.end_offset,
15488
15550
  resolved_at: resolvedAt
15489
15551
  };
15552
+ const provenance = sourceProvenance({
15553
+ source_ref: evidence.source_ref,
15554
+ source_uri: evidence.source_uri,
15555
+ source_kind: source.kind,
15556
+ source_revision_id: evidence.source_revision_id,
15557
+ revision: evidence.revision,
15558
+ hash: evidence.hash,
15559
+ chunk_id: row.id,
15560
+ start_offset: row.start_offset,
15561
+ end_offset: row.end_offset,
15562
+ status: metadataString(metadata, ["status"]),
15563
+ resolver: evidence.resolver
15564
+ });
15490
15565
  return {
15491
15566
  id: row.id,
15492
15567
  kind: row.kind,
@@ -15496,7 +15571,8 @@ async function resolveOpenFilesSource(options) {
15496
15571
  start_offset: row.start_offset,
15497
15572
  end_offset: row.end_offset,
15498
15573
  metadata,
15499
- evidence
15574
+ evidence,
15575
+ provenance
15500
15576
  };
15501
15577
  });
15502
15578
  const citations = chunks.map((chunk) => ({
@@ -15506,7 +15582,8 @@ async function resolveOpenFilesSource(options) {
15506
15582
  quote: chunk.text.slice(0, 500),
15507
15583
  start_offset: chunk.start_offset,
15508
15584
  end_offset: chunk.end_offset,
15509
- evidence: chunk.evidence
15585
+ evidence: chunk.evidence,
15586
+ provenance: chunk.provenance
15510
15587
  }));
15511
15588
  recordAuditEvent(db, {
15512
15589
  event_type: "source_read",
@@ -15922,13 +15999,188 @@ function providerStatus(config2, env = process.env) {
15922
15999
  };
15923
16000
  }
15924
16001
 
16002
+ // src/storage-contract.ts
16003
+ import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
16004
+ var GENERATED_ARTIFACTS = [
16005
+ {
16006
+ kind: "schema",
16007
+ prefix: "schemas/",
16008
+ description: "Machine-readable agent schemas and source rules."
16009
+ },
16010
+ {
16011
+ kind: "index",
16012
+ prefix: "indexes/",
16013
+ description: "Small orientation indexes and future shard manifests."
16014
+ },
16015
+ {
16016
+ kind: "log",
16017
+ prefix: "logs/",
16018
+ description: "Append-only JSONL run and wiki-maintenance log partitions."
16019
+ },
16020
+ {
16021
+ kind: "run",
16022
+ prefix: "runs/",
16023
+ description: "Prompt/tool/cost ledgers and generated output records."
16024
+ },
16025
+ {
16026
+ kind: "wiki_page",
16027
+ prefix: "wiki/",
16028
+ description: "Generated cited Markdown pages, not raw source files."
16029
+ },
16030
+ {
16031
+ kind: "export",
16032
+ prefix: "exports/",
16033
+ description: "Portable exports and snapshots of derived knowledge state."
16034
+ }
16035
+ ];
16036
+ function hashArtifactBody(body) {
16037
+ const bytes = typeof body === "string" ? Buffer.from(body) : Buffer.from(body);
16038
+ return {
16039
+ hash: `sha256:${createHash5("sha256").update(bytes).digest("hex")}`,
16040
+ size_bytes: bytes.byteLength
16041
+ };
16042
+ }
16043
+ function artifactKindForKey(key) {
16044
+ const match = GENERATED_ARTIFACTS.find((entry) => key.startsWith(entry.prefix));
16045
+ return match?.kind ?? "artifact";
16046
+ }
16047
+ function resolveStorageContract(config2, workspace, scope = "global") {
16048
+ const validation = validateStorageConfig(config2, workspace);
16049
+ const s3 = config2.storage.s3 ?? null;
16050
+ const prefix = s3?.prefix?.replace(/^\/+|\/+$/g, "") ?? "";
16051
+ const s3UriPrefix = s3 ? `s3://${s3.bucket}/${prefix ? `${prefix}/` : ""}` : "";
16052
+ return {
16053
+ scope,
16054
+ mode: config2.mode,
16055
+ storage_type: config2.storage.type,
16056
+ workspace_home: workspace.home,
16057
+ local_layout: {
16058
+ app_path: HASNA_KNOWLEDGE_APP_PATH,
16059
+ config_path: workspace.configPath,
16060
+ json_store_path: workspace.jsonStorePath,
16061
+ knowledge_db_path: workspace.knowledgeDbPath,
16062
+ directories: {
16063
+ artifacts: workspace.artifactsDir,
16064
+ cache: workspace.cacheDir,
16065
+ exports: workspace.exportsDir,
16066
+ indexes: workspace.indexesDir,
16067
+ logs: workspace.logsDir,
16068
+ runs: workspace.runsDir,
16069
+ schemas: workspace.schemasDir,
16070
+ wiki: workspace.wikiDir
16071
+ }
16072
+ },
16073
+ artifact_store: {
16074
+ type: config2.storage.type,
16075
+ artifacts_root: config2.storage.artifacts_root,
16076
+ uri_prefix: config2.storage.type === "s3" ? s3UriPrefix : `file://${workspace.artifactsDir}/`,
16077
+ s3: s3 ? {
16078
+ bucket: s3.bucket,
16079
+ prefix,
16080
+ region: s3.region ?? null,
16081
+ profile: s3.profile ?? null,
16082
+ server_side_encryption: s3.server_side_encryption ?? null,
16083
+ kms_key_configured: Boolean(s3.kms_key_id)
16084
+ } : null
16085
+ },
16086
+ source_ownership: {
16087
+ owner: "open-files",
16088
+ preferred_ref: config2.sources.preferred_ref,
16089
+ allowed_schemes: config2.sources.allowed_schemes,
16090
+ raw_source_bytes_stored_in_open_knowledge: false,
16091
+ stores: [
16092
+ "source refs",
16093
+ "source revisions and hashes",
16094
+ "citation spans",
16095
+ "redacted extracted chunks",
16096
+ "embeddings",
16097
+ "generated wiki artifacts",
16098
+ "indexes",
16099
+ "run ledgers"
16100
+ ],
16101
+ does_not_store: [
16102
+ "raw open-files bytes",
16103
+ "S3 object credentials",
16104
+ "connector secrets",
16105
+ "hosted tenant ownership state"
16106
+ ]
16107
+ },
16108
+ generated_artifacts: GENERATED_ARTIFACTS,
16109
+ scalability: {
16110
+ catalog: "knowledge.db tracks sources, revisions, chunks, citations, indexes, runs, and storage_objects.",
16111
+ indexes: "Indexes are cataloged DB rows plus sharded artifacts, not one giant index.md.",
16112
+ logs: "Logs use dated JSONL partitions under logs/yyyy/mm/dd.jsonl.",
16113
+ markdown: "Markdown pages are the readable wiki layer over DB/object-store state."
16114
+ },
16115
+ warnings: validation.warnings
16116
+ };
16117
+ }
16118
+ function validateStorageConfig(config2, workspace) {
16119
+ const errors3 = [];
16120
+ const warnings = [];
16121
+ if (!workspace.home.endsWith(HASNA_KNOWLEDGE_APP_PATH)) {
16122
+ warnings.push(`Workspace home does not end with ${HASNA_KNOWLEDGE_APP_PATH}: ${workspace.home}`);
16123
+ }
16124
+ if (config2.storage.type === "s3") {
16125
+ if (!config2.storage.s3?.bucket)
16126
+ errors3.push("storage.s3.bucket is required when storage.type is s3.");
16127
+ if (!config2.storage.s3?.prefix)
16128
+ warnings.push("storage.s3.prefix is empty; generated knowledge artifacts will be written at the bucket root.");
16129
+ if (config2.mode === "local")
16130
+ warnings.push("storage.type is s3 while mode is local; this is valid for BYO S3, but hosted wrappers should set mode to hosted.");
16131
+ }
16132
+ if (config2.storage.type === "local" && config2.storage.s3) {
16133
+ warnings.push("storage.s3 is configured but ignored while storage.type is local.");
16134
+ }
16135
+ if (config2.sources.preferred_ref !== "open-files") {
16136
+ warnings.push("sources.preferred_ref should stay open-files for durable company knowledge.");
16137
+ }
16138
+ if (!config2.sources.allowed_schemes.includes("open-files")) {
16139
+ errors3.push("sources.allowed_schemes must include open-files.");
16140
+ }
16141
+ return {
16142
+ ok: errors3.length === 0,
16143
+ errors: errors3,
16144
+ warnings
16145
+ };
16146
+ }
16147
+ function recordStorageObjects(db, objects, now = new Date) {
16148
+ const timestamp = now.toISOString();
16149
+ const statement = db.prepare(`
16150
+ INSERT INTO storage_objects (
16151
+ id, artifact_uri, kind, content_type, hash, size_bytes, metadata_json, created_at, updated_at
16152
+ )
16153
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
16154
+ ON CONFLICT(artifact_uri) DO UPDATE SET
16155
+ kind = excluded.kind,
16156
+ content_type = excluded.content_type,
16157
+ hash = excluded.hash,
16158
+ size_bytes = excluded.size_bytes,
16159
+ metadata_json = excluded.metadata_json,
16160
+ updated_at = excluded.updated_at
16161
+ `);
16162
+ const insert = db.transaction((entries) => {
16163
+ for (const entry of entries) {
16164
+ statement.run(randomUUID4(), entry.uri, entry.kind, entry.content_type ?? null, entry.hash ?? null, entry.size_bytes ?? null, JSON.stringify({
16165
+ key: entry.key,
16166
+ ...entry.metadata ?? {}
16167
+ }), timestamp, timestamp);
16168
+ }
16169
+ });
16170
+ insert(objects);
16171
+ }
16172
+
15925
16173
  // src/wiki-layout.ts
16174
+ import { createHash as createHash6 } from "crypto";
15926
16175
  function todayParts(now) {
15927
16176
  const year = String(now.getUTCFullYear());
15928
16177
  const month = String(now.getUTCMonth() + 1).padStart(2, "0");
15929
16178
  const day = String(now.getUTCDate()).padStart(2, "0");
15930
16179
  return { year, month, day };
15931
16180
  }
16181
+ function stableId3(prefix, value) {
16182
+ return `${prefix}_${createHash6("sha256").update(value).digest("hex").slice(0, 20)}`;
16183
+ }
15932
16184
  function agentSchemaTemplate() {
15933
16185
  return `# Knowledge Agent Schema v1
15934
16186
 
@@ -15996,22 +16248,99 @@ async function initializeWikiLayout(store, now = new Date) {
15996
16248
  root_index_key: rootIndexKey,
15997
16249
  wiki_readme_key: wikiReadmeKey
15998
16250
  };
15999
- const writes = [
16000
- store.put({ key: schemaKey, body: agentSchemaTemplate(), content_type: "text/markdown" }),
16001
- store.put({ key: rootIndexKey, body: rootIndexTemplate(), content_type: "text/markdown" }),
16002
- store.put({ key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: "text/markdown" }),
16003
- store.put({ key: logKey, body: `${JSON.stringify(event)}
16004
- `, content_type: "application/x-ndjson" })
16251
+ const entries = [
16252
+ { key: schemaKey, body: agentSchemaTemplate(), content_type: "text/markdown" },
16253
+ { key: rootIndexKey, body: rootIndexTemplate(), content_type: "text/markdown" },
16254
+ { key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: "text/markdown" },
16255
+ { key: logKey, body: `${JSON.stringify(event)}
16256
+ `, content_type: "application/x-ndjson" }
16005
16257
  ];
16006
- await Promise.all(writes);
16258
+ const artifacts = await Promise.all(entries.map(async (entry) => {
16259
+ const result = await store.put(entry);
16260
+ return {
16261
+ key: result.key,
16262
+ uri: result.uri,
16263
+ kind: artifactKindForKey(entry.key),
16264
+ content_type: entry.content_type,
16265
+ metadata: {
16266
+ provenance: generatedArtifactProvenance({
16267
+ generated_from: "wiki_layout_init",
16268
+ artifact_key: entry.key,
16269
+ citation_required: entry.key.startsWith("wiki/") || entry.key.startsWith("indexes/")
16270
+ })
16271
+ },
16272
+ ...hashArtifactBody(entry.body)
16273
+ };
16274
+ }));
16007
16275
  return {
16008
16276
  schema_key: schemaKey,
16009
16277
  root_index_key: rootIndexKey,
16010
16278
  wiki_readme_key: wikiReadmeKey,
16011
16279
  log_key: logKey,
16280
+ artifacts,
16012
16281
  written: [schemaKey, rootIndexKey, wikiReadmeKey, logKey]
16013
16282
  };
16014
16283
  }
16284
+ function provenanceFor(artifact) {
16285
+ const existing = artifact.metadata?.provenance;
16286
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
16287
+ return existing;
16288
+ }
16289
+ return generatedArtifactProvenance({
16290
+ generated_from: "wiki_layout_init",
16291
+ artifact_key: artifact.key
16292
+ });
16293
+ }
16294
+ function recordWikiLayoutCatalog(db, artifacts, now = new Date) {
16295
+ const timestamp = now.toISOString();
16296
+ const rootIndex = artifacts.find((artifact) => artifact.key.endsWith("indexes/root.md"));
16297
+ const wikiReadme = artifacts.find((artifact) => artifact.key.endsWith("wiki/README.md"));
16298
+ if (rootIndex) {
16299
+ db.run(`INSERT INTO knowledge_indexes (id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at)
16300
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
16301
+ ON CONFLICT(kind, name, shard_key) DO UPDATE SET
16302
+ artifact_uri = excluded.artifact_uri,
16303
+ metadata_json = excluded.metadata_json,
16304
+ updated_at = excluded.updated_at`, [
16305
+ stableId3("idx", "root:indexes/root.md"),
16306
+ "root",
16307
+ "root",
16308
+ rootIndex.uri,
16309
+ "root",
16310
+ JSON.stringify({
16311
+ artifact_key: rootIndex.key,
16312
+ content_hash: rootIndex.hash ?? null,
16313
+ provenance: provenanceFor(rootIndex)
16314
+ }),
16315
+ timestamp,
16316
+ timestamp
16317
+ ]);
16318
+ }
16319
+ if (wikiReadme) {
16320
+ db.run(`INSERT INTO wiki_pages (id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at)
16321
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
16322
+ ON CONFLICT(path) DO UPDATE SET
16323
+ title = excluded.title,
16324
+ artifact_uri = excluded.artifact_uri,
16325
+ content_hash = excluded.content_hash,
16326
+ status = excluded.status,
16327
+ metadata_json = excluded.metadata_json,
16328
+ updated_at = excluded.updated_at`, [
16329
+ stableId3("wiki", "wiki/README.md"),
16330
+ "wiki/README.md",
16331
+ "Wiki",
16332
+ wikiReadme.uri,
16333
+ wikiReadme.hash ?? null,
16334
+ "active",
16335
+ JSON.stringify({
16336
+ artifact_key: wikiReadme.key,
16337
+ provenance: provenanceFor(wikiReadme)
16338
+ }),
16339
+ timestamp,
16340
+ timestamp
16341
+ ]);
16342
+ }
16343
+ }
16015
16344
 
16016
16345
  // src/service.ts
16017
16346
  class KnowledgeService {
@@ -16048,6 +16377,12 @@ class KnowledgeService {
16048
16377
  artifactStore() {
16049
16378
  return createArtifactStore(this.config(), this.ensureWorkspace());
16050
16379
  }
16380
+ storageContract() {
16381
+ return resolveStorageContract(this.config(), this.ensureWorkspace(), this.scope);
16382
+ }
16383
+ validateStorage() {
16384
+ return validateStorageConfig(this.config(), this.ensureWorkspace());
16385
+ }
16051
16386
  paths() {
16052
16387
  const workspace = this.ensureWorkspace();
16053
16388
  return {
@@ -16076,7 +16411,17 @@ class KnowledgeService {
16076
16411
  return getKnowledgeDbStats(workspace.knowledgeDbPath);
16077
16412
  }
16078
16413
  async initWiki() {
16079
- return initializeWikiLayout(this.artifactStore());
16414
+ const workspace = this.ensureWorkspace();
16415
+ migrateKnowledgeDb(workspace.knowledgeDbPath);
16416
+ const result = await initializeWikiLayout(this.artifactStore());
16417
+ const db = openKnowledgeDb(workspace.knowledgeDbPath);
16418
+ try {
16419
+ recordStorageObjects(db, result.artifacts);
16420
+ recordWikiLayoutCatalog(db, result.artifacts);
16421
+ } finally {
16422
+ db.close();
16423
+ }
16424
+ return result;
16080
16425
  }
16081
16426
  async ingestManifest(input) {
16082
16427
  const workspace = this.ensureWorkspace();
@@ -16187,6 +16532,17 @@ function buildServer() {
16187
16532
  }, async ({ scope }) => {
16188
16533
  return jsonText(createKnowledgeService({ scope }).paths());
16189
16534
  });
16535
+ registerTool(server, "ok_storage_status", "Knowledge storage status", "Inspect local/S3 artifact storage, source ownership, and scalability contract", {
16536
+ scope: scopeField
16537
+ }, async ({ scope }) => {
16538
+ const service = createKnowledgeService({ scope });
16539
+ const validation = service.validateStorage();
16540
+ return jsonText({
16541
+ ok: validation.ok,
16542
+ ...service.storageContract(),
16543
+ validation
16544
+ });
16545
+ });
16190
16546
  registerTool(server, "ok_parse_source_ref", "Parse source reference", "Parse and validate an open-files, S3, file, or web source ref", {
16191
16547
  uri: exports_external.string().describe("Source reference URI")
16192
16548
  }, async ({ uri }) => {