@amoree-code/ocean 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/application/auth/auth-orchestrator.js +77 -0
- package/dist/application/clients/clients.js +646 -0
- package/dist/application/clients/editors.js +398 -0
- package/dist/application/clients/hooks.js +335 -0
- package/dist/application/clients/native.js +410 -0
- package/dist/application/clients/opencode-plugin.js +158 -0
- package/dist/application/clients/registry.js +188 -0
- package/dist/application/gateway/webhook-gateway.js +137 -0
- package/dist/application/install/provider-installer.js +184 -0
- package/dist/application/integrations/charter-copies.js +76 -0
- package/dist/application/integrations/claude-vscode.js +106 -0
- package/dist/application/integrations/engine-entry-repair.js +105 -0
- package/dist/application/integrations/skill-filter.js +254 -0
- package/dist/application/layout/layout-apply.js +443 -0
- package/dist/application/layout/layout-plan.js +236 -0
- package/dist/application/layout/workspace-template.js +34 -0
- package/dist/application/onboarding/state.js +23 -0
- package/dist/application/onboarding/steps.js +249 -0
- package/dist/application/profiles/profile-distribution.js +53 -0
- package/dist/application/profiles/profile-suggestion.js +39 -0
- package/dist/application/profiles/profile-upgrade-service.js +208 -0
- package/dist/application/pull/history-index.js +476 -0
- package/dist/application/pull/project-match.js +132 -0
- package/dist/application/pull/pull.js +202 -0
- package/dist/application/pull/server-capture.js +307 -0
- package/dist/application/scheduler/local-scheduler.js +210 -0
- package/dist/composition/runtime.js +64 -0
- package/dist/engine-root.js +9 -0
- package/dist/infrastructure/filesystem/profile-loader.js +46 -0
- package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
- package/dist/infrastructure/filesystem/skill-loader.js +84 -0
- package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
- package/dist/infrastructure/observability/runtime-logger.js +16 -0
- package/dist/infrastructure/process/agent-tool-executor.js +99 -0
- package/dist/infrastructure/process/cli-process.js +49 -0
- package/dist/infrastructure/process/interactive-process.js +145 -0
- package/dist/infrastructure/process/process-supervisor.js +194 -0
- package/dist/infrastructure/process/service.js +252 -0
- package/dist/infrastructure/providers/browser-provider.js +2 -0
- package/dist/infrastructure/providers/claude-sdk.js +137 -0
- package/dist/infrastructure/providers/client-home.js +14 -0
- package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
- package/dist/infrastructure/providers/ollama-embedder.js +74 -0
- package/dist/infrastructure/providers/openai-compatible.js +268 -0
- package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
- package/dist/infrastructure/providers/provider-capabilities.js +50 -0
- package/dist/infrastructure/providers/provider-invocation.js +184 -0
- package/dist/infrastructure/providers/provider-registry.js +77 -0
- package/dist/infrastructure/providers/providers.js +134 -0
- package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
- package/dist/interfaces/cli/auth-command.js +54 -0
- package/dist/interfaces/cli/browser-bridge-command.js +93 -0
- package/dist/interfaces/cli/browser-command.js +119 -0
- package/dist/interfaces/cli/capture-command.js +125 -0
- package/dist/interfaces/cli/client-test-command.js +133 -0
- package/dist/interfaces/cli/clients-command.js +129 -0
- package/dist/interfaces/cli/command-registry.js +1211 -0
- package/dist/interfaces/cli/context-command.js +172 -0
- package/dist/interfaces/cli/daily-command.js +131 -0
- package/dist/interfaces/cli/governance-command.js +55 -0
- package/dist/interfaces/cli/handoff-command.js +35 -0
- package/dist/interfaces/cli/help-command.js +207 -0
- package/dist/interfaces/cli/idea-command.js +47 -0
- package/dist/interfaces/cli/intercept-command.js +317 -0
- package/dist/interfaces/cli/layout-command.js +27 -0
- package/dist/interfaces/cli/market-command.js +52 -0
- package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
- package/dist/interfaces/cli/memory-command.js +317 -0
- package/dist/interfaces/cli/migrate-command.js +26 -0
- package/dist/interfaces/cli/observe-command.js +78 -0
- package/dist/interfaces/cli/onboard-command.js +123 -0
- package/dist/interfaces/cli/operate-command.js +59 -0
- package/dist/interfaces/cli/profiles-command.js +70 -0
- package/dist/interfaces/cli/pull-command.js +54 -0
- package/dist/interfaces/cli/setup-command.js +143 -0
- package/dist/interfaces/cli/tasks-command.js +249 -0
- package/dist/main.js +49 -0
- package/dist/node-warnings.js +17 -0
- package/dist/version.js +8 -0
- package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
- package/market/mcp/gateway/ocean.capability.json +21 -0
- package/market/mcp/obsidian/ocean.capability.json +21 -0
- package/market/mcp/ocean/ocean.capability.json +21 -0
- package/market/mcp/playwright/ocean.capability.json +23 -0
- package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
- package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
- package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
- package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
- package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
- package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
- package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
- package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
- package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
- package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
- package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
- package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
- package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
- package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
- package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
- package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
- package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
- package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
- package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
- package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
- package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
- package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
- package/node_modules/@ocean/brain/dist/memory/signal-detection.js +66 -0
- package/node_modules/@ocean/brain/package.json +11 -0
- package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
- package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
- package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
- package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
- package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
- package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
- package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
- package/node_modules/@ocean/context/package.json +11 -0
- package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
- package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
- package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
- package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
- package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
- package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
- package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
- package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
- package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
- package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
- package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
- package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
- package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
- package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
- package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
- package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
- package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
- package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
- package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
- package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
- package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
- package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
- package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
- package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
- package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
- package/node_modules/@ocean/core/package.json +14 -0
- package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
- package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
- package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
- package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
- package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
- package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
- package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
- package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
- package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
- package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
- package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
- package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
- package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
- package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
- package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
- package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
- package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
- package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
- package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
- package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
- package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
- package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
- package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
- package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
- package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
- package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
- package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
- package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
- package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
- package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
- package/node_modules/@ocean/loop/package.json +11 -0
- package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
- package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
- package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
- package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
- package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
- package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
- package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
- package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
- package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
- package/node_modules/@ocean/market/dist/install/lock.js +34 -0
- package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
- package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
- package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
- package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
- package/node_modules/@ocean/market/dist/security/sandbox.js +74 -0
- package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
- package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
- package/node_modules/@ocean/market/dist/skills/skill-curation.js +123 -0
- package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
- package/node_modules/@ocean/market/dist/skills/task-observer.js +165 -0
- package/node_modules/@ocean/market/package.json +11 -0
- package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
- package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
- package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
- package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
- package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
- package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
- package/node_modules/@ocean/services/package.json +11 -0
- package/ocean-marketplace.json +415 -0
- package/package.json +67 -4
- package/skills/core/business-logic/SKILL.md +22 -0
- package/skills/core/catch-up/SKILL.md +17 -0
- package/skills/core/core-thinking/SKILL.md +11 -0
- package/skills/core/design-thinking/SKILL.md +19 -0
- package/skills/core/graft/SKILL.md +156 -0
- package/skills/core/loop/SKILL.md +20 -0
- package/skills/core/session-handoff/SKILL.md +15 -0
- package/skills/core/use-browser/SKILL.md +69 -0
- package/skills/core/verification/SKILL.md +11 -0
- package/skills/index.json +56 -0
- package/templates/README.md +4 -0
- package/templates/profiles/default.json +51 -0
- package/templates/profiles/developer.json +51 -0
- package/templates/profiles/reviewer.json +51 -0
- package/templates/profiles/strategist.json +51 -0
- package/templates/workspace/00-inbox/INBOX.md +7 -0
- package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
- package/templates/workspace/00-inbox/native/.gitkeep +0 -0
- package/templates/workspace/01-daily/.gitkeep +0 -0
- package/templates/workspace/02-personal/CONFLICTS.md +7 -0
- package/templates/workspace/02-personal/MEMORY.md +15 -0
- package/templates/workspace/02-personal/goals/.gitkeep +0 -0
- package/templates/workspace/02-personal/identity/.gitkeep +0 -0
- package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
- package/templates/workspace/03-professional/.gitkeep +0 -0
- package/templates/workspace/04-projects/PROJECTS.md +5 -0
- package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
- package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
- package/templates/workspace/06-templates/TEMPLATES.md +25 -0
- package/templates/workspace/06-templates/decision.md +8 -0
- package/templates/workspace/06-templates/intelligence.md +32 -0
- package/templates/workspace/06-templates/project-claude.md +48 -0
- package/templates/workspace/06-templates/session.md +38 -0
- package/templates/workspace/06-templates/task.md +42 -0
- package/templates/workspace/MANIFEST.json +51 -0
- package/templates/workspace/bridge/config/CONFIG.md +21 -0
- package/templates/workspace/bridge/config/runtime.json +4 -0
- package/templates/workspace/charter/core.md +104 -0
- package/templates/workspace/charter/policies/README.md +31 -0
- package/templates/workspace/charter/policies/claude-code-context.md +90 -0
- package/templates/workspace/charter/policies/concise-output.md +29 -0
- package/templates/workspace/charter/policies/context.md +91 -0
- package/templates/workspace/charter/policies/contracts/task.md +189 -0
- package/templates/workspace/charter/policies/git.md +60 -0
- package/templates/workspace/charter/policies/graph.md +47 -0
- package/templates/workspace/charter/policies/knowledge.md +33 -0
- package/templates/workspace/charter/policies/lifecycle.md +119 -0
- package/templates/workspace/charter/policies/memory.md +67 -0
- package/templates/workspace/charter/policies/minimal-change.md +43 -0
- package/templates/workspace/charter/policies/models.md +57 -0
- package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
- package/templates/workspace/charter/policies/response.md +39 -0
- package/templates/workspace/charter/policies/skill-observer.md +44 -0
- package/templates/workspace/charter/policies/stack.md +23 -0
- package/templates/workspace/charter/policies/task.md +139 -0
- package/templates/workspace/charter/policies/verification.md +29 -0
- package/templates/workspace/sessions/.gitkeep +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { scopeVisible, visibleScopes, } from "@ocean/core/domain/brain/record-scope.js";
|
|
4
|
+
import { BRAIN_INDEX_SCHEMA_VERSION, } from "@ocean/core/domain/ports/brain-index-port.js";
|
|
5
|
+
import { oceanRoot, resolveStorePath, resolveWithin, STORE_DIR, storeRelativeToRoot, } from "@ocean/core/shared/paths.js";
|
|
6
|
+
import { normalizeForSearch, normalizeRecord } from "./brain-markdown.js";
|
|
7
|
+
import { brainIndexPath, computeCorpusHash, reindexBrain, } from "./brain-reindex.js";
|
|
8
|
+
// A v1 index has no scope column, so it cannot answer a scoped query: say how to fix it
|
|
9
|
+
// rather than return unfiltered hits.
|
|
10
|
+
export class BrainIndexStaleError extends Error {
|
|
11
|
+
}
|
|
12
|
+
function assertScoped(reader) {
|
|
13
|
+
if (reader.meta.schemaVersion !== BRAIN_INDEX_SCHEMA_VERSION)
|
|
14
|
+
throw new BrainIndexStaleError(`brain index schema ${reader.meta.schemaVersion} predates record scopes — run \`ocean memory reindex\``);
|
|
15
|
+
}
|
|
16
|
+
const RRF_K = 60;
|
|
17
|
+
function snippetOf(text, maxLen = 240) {
|
|
18
|
+
const trimmed = text.trim().replace(/\s+/g, " ");
|
|
19
|
+
return trimmed.length > maxLen ? `${trimmed.slice(0, maxLen - 1)}…` : trimmed;
|
|
20
|
+
}
|
|
21
|
+
export async function brainSearch(options) {
|
|
22
|
+
const root = options.root ?? oceanRoot();
|
|
23
|
+
const limit = Math.max(1, Math.min(20, options.limit ?? 8));
|
|
24
|
+
const indexFile = brainIndexPath(root);
|
|
25
|
+
let exists = true;
|
|
26
|
+
try {
|
|
27
|
+
await stat(indexFile);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
exists = false;
|
|
31
|
+
}
|
|
32
|
+
if (!exists) {
|
|
33
|
+
throw new Error("brain index not built — run ocean memory reindex");
|
|
34
|
+
}
|
|
35
|
+
const reader = options.indexPort.openIndexReadOnly(indexFile);
|
|
36
|
+
try {
|
|
37
|
+
if (options.scope)
|
|
38
|
+
assertScoped(reader);
|
|
39
|
+
const scopes = options.scope
|
|
40
|
+
? visibleScopes(options.scope.project)
|
|
41
|
+
: undefined;
|
|
42
|
+
const normalizedQuery = normalizeForSearch(options.query);
|
|
43
|
+
const ftsHits = reader.ftsQuery(normalizedQuery, limit * 4, scopes);
|
|
44
|
+
let vecHits = [];
|
|
45
|
+
let vectorStatus = "unavailable: index built without an embedder";
|
|
46
|
+
if (reader.hasVectors && options.embedder) {
|
|
47
|
+
try {
|
|
48
|
+
const [embedding] = await options.embedder.embed([normalizedQuery]);
|
|
49
|
+
// KNN cannot filter by scope before k, so a scoped query oversamples: other projects'
|
|
50
|
+
// chunks would otherwise fill the top hits and the backstop below would drop them all.
|
|
51
|
+
vecHits = reader.vecQuery(embedding, options.scope ? limit * 16 : limit * 4);
|
|
52
|
+
vectorStatus = "ok";
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
vectorStatus = `unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (reader.hasVectors && !options.embedder) {
|
|
59
|
+
vectorStatus = "unavailable: no embedder supplied to this query";
|
|
60
|
+
}
|
|
61
|
+
// Reciprocal rank fusion: rank position (1-based), not raw score, so bm25 and cosine
|
|
62
|
+
// distance — on incompatible scales — combine without one dominating by magnitude.
|
|
63
|
+
const fused = new Map();
|
|
64
|
+
ftsHits.forEach((hit, index) => {
|
|
65
|
+
const entry = fused.get(hit.chunkId) ?? {
|
|
66
|
+
docId: hit.docId,
|
|
67
|
+
score: 0,
|
|
68
|
+
fts: false,
|
|
69
|
+
vec: false,
|
|
70
|
+
};
|
|
71
|
+
entry.score += 1 / (RRF_K + index + 1);
|
|
72
|
+
entry.fts = true;
|
|
73
|
+
fused.set(hit.chunkId, entry);
|
|
74
|
+
});
|
|
75
|
+
vecHits.forEach((hit, index) => {
|
|
76
|
+
const entry = fused.get(hit.chunkId) ?? {
|
|
77
|
+
docId: hit.docId,
|
|
78
|
+
score: 0,
|
|
79
|
+
fts: false,
|
|
80
|
+
vec: false,
|
|
81
|
+
};
|
|
82
|
+
entry.score += 1 / (RRF_K + index + 1);
|
|
83
|
+
entry.vec = true;
|
|
84
|
+
fused.set(hit.chunkId, entry);
|
|
85
|
+
});
|
|
86
|
+
const byDoc = new Map();
|
|
87
|
+
for (const [chunkId, entry] of fused) {
|
|
88
|
+
const existing = byDoc.get(entry.docId);
|
|
89
|
+
if (!existing || entry.score > existing.score) {
|
|
90
|
+
byDoc.set(entry.docId, {
|
|
91
|
+
chunkId,
|
|
92
|
+
score: entry.score,
|
|
93
|
+
fts: entry.fts,
|
|
94
|
+
vec: entry.vec,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const ranked = [...byDoc.entries()]
|
|
99
|
+
.map(([docId, entry]) => ({ docId, ...entry }))
|
|
100
|
+
.sort((a, b) => b.score - a.score || a.docId - b.docId);
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const item of ranked) {
|
|
103
|
+
const doc = reader.getDoc(item.docId);
|
|
104
|
+
if (!doc)
|
|
105
|
+
continue;
|
|
106
|
+
if (options.pathPrefix &&
|
|
107
|
+
!String(doc.path).startsWith(options.pathPrefix))
|
|
108
|
+
continue;
|
|
109
|
+
if (options.types?.length && !options.types.includes(String(doc.type)))
|
|
110
|
+
continue;
|
|
111
|
+
// Backstop for hits the SQL filter never saw (vector hits are not scope-filtered).
|
|
112
|
+
if (options.scope &&
|
|
113
|
+
!scopeVisible(String(doc.scope), options.scope.project))
|
|
114
|
+
continue;
|
|
115
|
+
const chunk = reader.getChunk(item.chunkId);
|
|
116
|
+
results.push({
|
|
117
|
+
id: String(doc.id),
|
|
118
|
+
path: String(doc.path),
|
|
119
|
+
file: storeRelativeToRoot(String(doc.path), root),
|
|
120
|
+
updated: doc.updated ?? null,
|
|
121
|
+
title: String(doc.title),
|
|
122
|
+
summary: String(doc.summary),
|
|
123
|
+
type: String(doc.type),
|
|
124
|
+
scope: String(doc.scope ?? "global"),
|
|
125
|
+
confidence: String(doc.confidence),
|
|
126
|
+
lastConfirmedAt: doc.last_confirmed_at ?? null,
|
|
127
|
+
heading: chunk ? (chunk.heading ?? null) : null,
|
|
128
|
+
snippet: chunk ? snippetOf(String(chunk.text)) : "",
|
|
129
|
+
score: Math.round(item.score * 1_000_000) / 1_000_000,
|
|
130
|
+
matchedBy: item.fts && item.vec ? "both" : item.vec ? "vector" : "fts",
|
|
131
|
+
});
|
|
132
|
+
if (results.length >= limit)
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const stale = reader.meta.corpusHash !== (await computeCorpusHash(root));
|
|
136
|
+
return {
|
|
137
|
+
mode: reader.hasVectors && options.embedder && vectorStatus === "ok"
|
|
138
|
+
? "hybrid"
|
|
139
|
+
: "fts",
|
|
140
|
+
stale,
|
|
141
|
+
vector: vectorStatus,
|
|
142
|
+
results,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
reader.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Always reads the markdown FILE, never the index row — the index can be stale or (in a
|
|
150
|
+
// test) deliberately tampered with, and brain_read must never surface a lie about what's on
|
|
151
|
+
// disk. The index is used only to resolve id -> path.
|
|
152
|
+
export async function brainRead(options) {
|
|
153
|
+
const root = options.root ?? oceanRoot();
|
|
154
|
+
const maxBytes = Math.max(512, Math.min(16_000, options.maxBytes ?? 8_000));
|
|
155
|
+
const indexFile = brainIndexPath(root);
|
|
156
|
+
const reader = options.indexPort.openIndexReadOnly(indexFile);
|
|
157
|
+
let docRow;
|
|
158
|
+
try {
|
|
159
|
+
// Clients also cite a search result's path without its extension.
|
|
160
|
+
const candidates = options.idOrPath.endsWith(".md")
|
|
161
|
+
? [options.idOrPath]
|
|
162
|
+
: [options.idOrPath, `${options.idOrPath}.md`];
|
|
163
|
+
for (const candidate of candidates) {
|
|
164
|
+
docRow = reader.getDocByIdOrPath(candidate);
|
|
165
|
+
if (!docRow) {
|
|
166
|
+
const storePath = indexedStorePath(candidate, root);
|
|
167
|
+
if (storePath)
|
|
168
|
+
docRow = reader.getDocByIdOrPath(storePath);
|
|
169
|
+
}
|
|
170
|
+
if (docRow)
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
reader.close();
|
|
176
|
+
}
|
|
177
|
+
if (!docRow)
|
|
178
|
+
throw new Error(`no brain record found for '${options.idOrPath}'`);
|
|
179
|
+
// The index is only ever used above to resolve id/path -> the store-relative path. Every
|
|
180
|
+
// field returned below is re-derived from the file's own bytes on disk (normalizeRecord),
|
|
181
|
+
// never from the (possibly stale or tampered) index row — this is what makes brain_read
|
|
182
|
+
// safe to trust even when the index itself cannot be.
|
|
183
|
+
const relativePath = String(docRow.path);
|
|
184
|
+
const filePath = resolveStorePath(root, relativePath);
|
|
185
|
+
const raw = await readFile(filePath, "utf8");
|
|
186
|
+
const { record, body } = normalizeRecord(relativePath, raw);
|
|
187
|
+
const truncated = Buffer.byteLength(body, "utf8") > maxBytes;
|
|
188
|
+
const slicedBody = truncated
|
|
189
|
+
? Buffer.from(body).subarray(0, maxBytes).toString("utf8")
|
|
190
|
+
: body;
|
|
191
|
+
return {
|
|
192
|
+
id: record.id,
|
|
193
|
+
path: relativePath,
|
|
194
|
+
title: record.title,
|
|
195
|
+
summary: record.summary,
|
|
196
|
+
tags: record.tags,
|
|
197
|
+
type: record.type,
|
|
198
|
+
scope: record.scope,
|
|
199
|
+
confidence: record.confidence,
|
|
200
|
+
created: record.created,
|
|
201
|
+
updated: record.updated,
|
|
202
|
+
lastConfirmedAt: record.lastConfirmedAt,
|
|
203
|
+
body: slicedBody,
|
|
204
|
+
truncated,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export async function brainNeighbors(options) {
|
|
208
|
+
const root = options.root ?? oceanRoot();
|
|
209
|
+
const depth = options.depth ?? 1;
|
|
210
|
+
const direction = options.direction ?? "both";
|
|
211
|
+
const limit = Math.max(1, Math.min(50, options.limit ?? 20));
|
|
212
|
+
const indexFile = brainIndexPath(root);
|
|
213
|
+
const reader = options.indexPort.openIndexReadOnly(indexFile);
|
|
214
|
+
try {
|
|
215
|
+
if (options.scope)
|
|
216
|
+
assertScoped(reader);
|
|
217
|
+
const visible = (doc) => !options.scope || scopeVisible(String(doc.scope), options.scope.project);
|
|
218
|
+
const start = reader.getDocByIdOrPath(options.idOrPath);
|
|
219
|
+
if (!start || !visible(start))
|
|
220
|
+
throw new Error(`no brain record found for '${options.idOrPath}'`);
|
|
221
|
+
const visited = new Set([Number(start.doc_id)]);
|
|
222
|
+
const neighbors = [];
|
|
223
|
+
let frontier = [Number(start.doc_id)];
|
|
224
|
+
for (let level = 0; level < depth && neighbors.length < limit; level += 1) {
|
|
225
|
+
const next = [];
|
|
226
|
+
for (const docId of frontier) {
|
|
227
|
+
const directions = direction === "both" ? ["out", "in"] : [direction];
|
|
228
|
+
for (const dir of directions) {
|
|
229
|
+
for (const link of reader.linksFor(docId, dir)) {
|
|
230
|
+
const targetId = dir === "out" ? link.targetDocId : link.sourceDocId;
|
|
231
|
+
if (targetId === null ||
|
|
232
|
+
targetId === undefined ||
|
|
233
|
+
visited.has(targetId))
|
|
234
|
+
continue;
|
|
235
|
+
visited.add(targetId);
|
|
236
|
+
const doc = reader.getDoc(targetId);
|
|
237
|
+
if (!doc || !visible(doc))
|
|
238
|
+
continue;
|
|
239
|
+
neighbors.push({
|
|
240
|
+
id: String(doc.id),
|
|
241
|
+
path: String(doc.path),
|
|
242
|
+
title: String(doc.title),
|
|
243
|
+
via: dir,
|
|
244
|
+
});
|
|
245
|
+
next.push(targetId);
|
|
246
|
+
if (neighbors.length >= limit)
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
if (neighbors.length >= limit)
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
if (neighbors.length >= limit)
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
frontier = next;
|
|
256
|
+
}
|
|
257
|
+
return { neighbors };
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
reader.close();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Whether the index is missing or older than the files it was built from, and whether it holds
|
|
264
|
+
// vectors — what an automatic rebuild needs to decide (T-307).
|
|
265
|
+
export async function brainIndexState(options) {
|
|
266
|
+
const root = options.root ?? oceanRoot();
|
|
267
|
+
const indexFile = brainIndexPath(root);
|
|
268
|
+
if (!(await ensureIndexBuilt(root)))
|
|
269
|
+
return { stale: true, hasVectors: false };
|
|
270
|
+
const reader = options.indexPort.openIndexReadOnly(indexFile);
|
|
271
|
+
try {
|
|
272
|
+
return {
|
|
273
|
+
stale: reader.meta.corpusHash !== (await computeCorpusHash(root)),
|
|
274
|
+
hasVectors: reader.hasVectors,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
reader.close();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export async function ensureIndexBuilt(root = oceanRoot()) {
|
|
282
|
+
try {
|
|
283
|
+
await stat(brainIndexPath(root));
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export { reindexBrain };
|
|
291
|
+
export function relativeStorePath(file, root = oceanRoot()) {
|
|
292
|
+
for (const [store, dir] of Object.entries(STORE_DIR)) {
|
|
293
|
+
const relative = path.relative(resolveWithin(root, dir), file);
|
|
294
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
295
|
+
return `${store}/${relative.split(path.sep).join("/")}`;
|
|
296
|
+
}
|
|
297
|
+
return path.relative(root, file).split(path.sep).join("/");
|
|
298
|
+
}
|
|
299
|
+
function indexedStorePath(input, root) {
|
|
300
|
+
let relative = input.replaceAll("\\", "/");
|
|
301
|
+
if (path.isAbsolute(input)) {
|
|
302
|
+
relative = path.relative(root, input).split(path.sep).join("/");
|
|
303
|
+
if (relative === ".." ||
|
|
304
|
+
relative.startsWith("../") ||
|
|
305
|
+
path.isAbsolute(relative))
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
if (relative.startsWith("02-personal/"))
|
|
309
|
+
return `memory/${relative.slice("02-personal/".length)}`;
|
|
310
|
+
if (relative.startsWith("05-knowledge/"))
|
|
311
|
+
return `knowledge/${relative.slice("05-knowledge/".length)}`;
|
|
312
|
+
const projectMemory = relative.match(/^04-projects\/([^/]+)\/memory\/(.+)$/);
|
|
313
|
+
if (projectMemory)
|
|
314
|
+
return `project/${projectMemory[1]}/${projectMemory[2]}`;
|
|
315
|
+
const projectDocs = relative.match(/^04-projects\/([^/]+)\/docs\/(.+)$/);
|
|
316
|
+
if (projectDocs)
|
|
317
|
+
return `project-docs/${projectDocs[1]}/${projectDocs[2]}`;
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=brain-service.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { INBOX_DIR, oceanPath } from "@ocean/core/shared/paths.js";
|
|
3
|
+
const marker = "## Capture candidates (generated)";
|
|
4
|
+
export async function syncCaptureInbox(store) {
|
|
5
|
+
const file = oceanPath(INBOX_DIR, "INBOX.md");
|
|
6
|
+
let current;
|
|
7
|
+
try {
|
|
8
|
+
current = await readFile(file, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const base = current.split(marker)[0].trimEnd();
|
|
14
|
+
const items = store.listCaptureItems("new");
|
|
15
|
+
const generated = items.length === 0
|
|
16
|
+
? `${marker}\n\n_No new capture candidates._`
|
|
17
|
+
: `${marker}\n\n${items.map((item) => `- [${item.captureId}] ${item.content.replace(/\s+/g, " ").trim()} _(session ${item.sessionId})_`).join("\n")}`;
|
|
18
|
+
const next = `${base}\n\n${generated}\n`;
|
|
19
|
+
if (next !== current)
|
|
20
|
+
await writeFile(file, next, "utf8");
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=inbox-sync.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { evidenceSchema, } from "@ocean/core/domain/evidence/evidence.js";
|
|
3
|
+
export function buildVerificationReport(records) {
|
|
4
|
+
const evidence = records.map((record) => evidenceSchema.parse(record));
|
|
5
|
+
const proven = evidence.filter((record) => record.result === "proven");
|
|
6
|
+
const notProven = evidence.filter((record) => record.result === "not_proven");
|
|
7
|
+
const limitations = evidence.filter((record) => record.result === "limitation");
|
|
8
|
+
const fingerprint = createHash("sha256")
|
|
9
|
+
.update(JSON.stringify({ proven, notProven, limitations }))
|
|
10
|
+
.digest("hex");
|
|
11
|
+
return { proven, notProven, limitations, fingerprint };
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=verification-report.js.map
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { rename, rm } from "node:fs/promises";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { BRAIN_INDEX_SCHEMA_VERSION, } from "@ocean/core/domain/ports/brain-index-port.js";
|
|
4
|
+
import * as sqliteVec from "sqlite-vec";
|
|
5
|
+
function createSchema(db, withVectors, dims) {
|
|
6
|
+
db.exec(`
|
|
7
|
+
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
8
|
+
CREATE TABLE docs (
|
|
9
|
+
doc_id INTEGER PRIMARY KEY,
|
|
10
|
+
id TEXT NOT NULL UNIQUE,
|
|
11
|
+
path TEXT NOT NULL UNIQUE,
|
|
12
|
+
title TEXT NOT NULL,
|
|
13
|
+
summary TEXT NOT NULL,
|
|
14
|
+
tags_json TEXT NOT NULL,
|
|
15
|
+
type TEXT NOT NULL,
|
|
16
|
+
scope TEXT NOT NULL,
|
|
17
|
+
confidence TEXT NOT NULL,
|
|
18
|
+
created TEXT,
|
|
19
|
+
updated TEXT,
|
|
20
|
+
last_confirmed_at TEXT,
|
|
21
|
+
content_hash TEXT NOT NULL,
|
|
22
|
+
bytes INTEGER NOT NULL,
|
|
23
|
+
conformance_json TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE chunks (
|
|
26
|
+
chunk_id INTEGER PRIMARY KEY,
|
|
27
|
+
doc_id INTEGER NOT NULL REFERENCES docs(doc_id),
|
|
28
|
+
ordinal INTEGER NOT NULL,
|
|
29
|
+
heading TEXT,
|
|
30
|
+
text TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
CREATE VIRTUAL TABLE chunks_fts USING fts5(
|
|
33
|
+
title, heading, body,
|
|
34
|
+
tokenize = "unicode61 remove_diacritics 2",
|
|
35
|
+
content = ""
|
|
36
|
+
);
|
|
37
|
+
CREATE TABLE links (
|
|
38
|
+
source_doc_id INTEGER NOT NULL REFERENCES docs(doc_id),
|
|
39
|
+
target_raw TEXT NOT NULL,
|
|
40
|
+
target_doc_id INTEGER,
|
|
41
|
+
PRIMARY KEY (source_doc_id, target_raw)
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX links_target_doc_id ON links(target_doc_id);
|
|
44
|
+
CREATE INDEX chunks_doc_id ON chunks(doc_id);
|
|
45
|
+
`);
|
|
46
|
+
if (withVectors) {
|
|
47
|
+
db.exec(`CREATE VIRTUAL TABLE chunks_vec USING vec0(embedding float[${dims}] distance_metric=cosine);`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Inserts sorted-by-path (docs already arrive sorted from the caller) in one transaction so
|
|
51
|
+
// rowids — and therefore FTS/vec0 rowids — are deterministic across an identical rebuild.
|
|
52
|
+
async function populate(db, docs, links, options) {
|
|
53
|
+
const insertDoc = db.prepare(`
|
|
54
|
+
INSERT INTO docs (id, path, title, summary, tags_json, type, scope, confidence, created, updated,
|
|
55
|
+
last_confirmed_at, content_hash, bytes, conformance_json)
|
|
56
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
57
|
+
`);
|
|
58
|
+
const insertChunk = db.prepare(`
|
|
59
|
+
INSERT INTO chunks (doc_id, ordinal, heading, text) VALUES (?, ?, ?, ?)
|
|
60
|
+
`);
|
|
61
|
+
const insertFts = db.prepare(`
|
|
62
|
+
INSERT INTO chunks_fts (rowid, title, heading, body) VALUES (?, ?, ?, ?)
|
|
63
|
+
`);
|
|
64
|
+
// A document can legitimately link to the same target twice (e.g. two separate mentions
|
|
65
|
+
// of [[MEMORY.md]]); the second is a no-op, not a conflict — the same (source, target_raw)
|
|
66
|
+
// pair carries no extra information the second time.
|
|
67
|
+
const insertLink = db.prepare(`
|
|
68
|
+
INSERT OR IGNORE INTO links (source_doc_id, target_raw, target_doc_id) VALUES (?, ?, ?)
|
|
69
|
+
`);
|
|
70
|
+
const pathToDocId = new Map();
|
|
71
|
+
const chunkEmbedInputs = [];
|
|
72
|
+
db.exec("BEGIN");
|
|
73
|
+
try {
|
|
74
|
+
for (const doc of docs) {
|
|
75
|
+
insertDoc.run(doc.id, doc.path, doc.title, doc.summary, JSON.stringify(doc.tags), doc.type, doc.scope, doc.confidence, doc.created, doc.updated, doc.lastConfirmedAt, doc.contentHash, doc.bytes, JSON.stringify(doc.conformance));
|
|
76
|
+
const docIdRow = db.prepare("SELECT last_insert_rowid() AS id").get();
|
|
77
|
+
const docId = Number(docIdRow.id);
|
|
78
|
+
pathToDocId.set(doc.path, docId);
|
|
79
|
+
for (const chunk of doc.chunks) {
|
|
80
|
+
insertChunk.run(docId, chunk.ordinal, chunk.heading, chunk.text);
|
|
81
|
+
const chunkIdRow = db
|
|
82
|
+
.prepare("SELECT last_insert_rowid() AS id")
|
|
83
|
+
.get();
|
|
84
|
+
const chunkId = Number(chunkIdRow.id);
|
|
85
|
+
insertFts.run(chunkId, doc.title.toLowerCase(), (chunk.heading ?? "").toLowerCase(), chunk.normalizedText);
|
|
86
|
+
if (options.embedder)
|
|
87
|
+
chunkEmbedInputs.push({
|
|
88
|
+
chunkId: BigInt(chunkId),
|
|
89
|
+
text: chunk.normalizedText,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const link of links) {
|
|
94
|
+
const sourceDocId = pathToDocId.get(link.sourcePath);
|
|
95
|
+
if (sourceDocId === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
const targetDocId = link.targetPath
|
|
98
|
+
? (pathToDocId.get(link.targetPath) ?? null)
|
|
99
|
+
: null;
|
|
100
|
+
insertLink.run(sourceDocId, link.targetRaw, targetDocId);
|
|
101
|
+
}
|
|
102
|
+
db.exec("COMMIT");
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
db.exec("ROLLBACK");
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
if (options.embedder && chunkEmbedInputs.length > 0) {
|
|
109
|
+
const embeddings = await options.embedder.embed(chunkEmbedInputs.map((item) => item.text));
|
|
110
|
+
const insertVec = db.prepare("INSERT INTO chunks_vec (rowid, embedding) VALUES (?, ?)");
|
|
111
|
+
db.exec("BEGIN");
|
|
112
|
+
try {
|
|
113
|
+
chunkEmbedInputs.forEach((item, index) => {
|
|
114
|
+
insertVec.run(item.chunkId, embeddings[index]);
|
|
115
|
+
});
|
|
116
|
+
db.exec("COMMIT");
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
db.exec("ROLLBACK");
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)").run("schema_version", BRAIN_INDEX_SCHEMA_VERSION);
|
|
124
|
+
db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)").run("embedder", options.embedder?.model ?? "");
|
|
125
|
+
db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)").run("dims", String(options.embedder?.dimensions ?? 0));
|
|
126
|
+
db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)").run("corpus_hash", options.corpusHash);
|
|
127
|
+
db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)").run("built_at", new Date().toISOString());
|
|
128
|
+
}
|
|
129
|
+
// Builds a brand-new index file at `outFile` from scratch: writes into a sibling temp file
|
|
130
|
+
// and renames atomically into place, so a reader never observes a half-built index and a
|
|
131
|
+
// crash mid-build never corrupts whatever index was there before.
|
|
132
|
+
export async function buildIndex(docs, links, outFile, options) {
|
|
133
|
+
const temp = `${outFile}.tmp-${process.pid}-${Date.now()}`;
|
|
134
|
+
await rm(temp, { force: true });
|
|
135
|
+
const db = new DatabaseSync(temp, { allowExtension: true });
|
|
136
|
+
try {
|
|
137
|
+
if (options.embedder)
|
|
138
|
+
sqliteVec.load(db);
|
|
139
|
+
createSchema(db, Boolean(options.embedder), options.embedder?.dimensions ?? 0);
|
|
140
|
+
await populate(db, docs, links, options);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
db.close();
|
|
144
|
+
}
|
|
145
|
+
await rename(temp, outFile);
|
|
146
|
+
}
|
|
147
|
+
function loadVectorExtension(db) {
|
|
148
|
+
try {
|
|
149
|
+
sqliteVec.load(db);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export class BrainIndexReader {
|
|
157
|
+
db;
|
|
158
|
+
meta;
|
|
159
|
+
hasVectors;
|
|
160
|
+
constructor(file) {
|
|
161
|
+
this.db = new DatabaseSync(file, { readOnly: true, allowExtension: true });
|
|
162
|
+
this.db.exec("PRAGMA query_only = ON;");
|
|
163
|
+
const rows = this.db.prepare("SELECT key, value FROM meta").all();
|
|
164
|
+
const map = Object.fromEntries(rows.map((row) => [row.key, row.value]));
|
|
165
|
+
this.meta = {
|
|
166
|
+
schemaVersion: map.schema_version ?? "",
|
|
167
|
+
embedder: map.embedder ?? "",
|
|
168
|
+
dims: Number(map.dims ?? 0),
|
|
169
|
+
corpusHash: map.corpus_hash ?? "",
|
|
170
|
+
builtAt: map.built_at ?? "",
|
|
171
|
+
};
|
|
172
|
+
// sqlite-vec ships one prebuilt per platform; where none installed, search stays full-text.
|
|
173
|
+
this.hasVectors = this.meta.dims > 0 && loadVectorExtension(this.db);
|
|
174
|
+
}
|
|
175
|
+
ftsQuery(normalizedQuery, limit, scopes) {
|
|
176
|
+
// Tokens are matched as quoted prefixes (`"token"*`), ANDed together (FTS5's default):
|
|
177
|
+
// exact-token phrase matching is too strict for agglutinative languages like Sorani,
|
|
178
|
+
// where a query word is frequently a prefix of the inflected form actually stored.
|
|
179
|
+
const words = normalizedQuery
|
|
180
|
+
.split(/[^\p{L}\p{N}_-]+/u)
|
|
181
|
+
.map((token) => token.trim())
|
|
182
|
+
.filter(Boolean);
|
|
183
|
+
const quote = (token) => `"${token.replace(/"/g, '""')}"*`;
|
|
184
|
+
if (words.length === 0)
|
|
185
|
+
return [];
|
|
186
|
+
// Filtered in SQL, before LIMIT, so another project's records cannot crowd out the hits.
|
|
187
|
+
const scopeFilter = scopes
|
|
188
|
+
? ` AND c.doc_id IN (SELECT doc_id FROM docs WHERE scope IN (${scopes.map(() => "?").join(",")}))`
|
|
189
|
+
: "";
|
|
190
|
+
const run = (match, max) => {
|
|
191
|
+
try {
|
|
192
|
+
return this.db
|
|
193
|
+
.prepare(`
|
|
194
|
+
SELECT c.chunk_id AS chunkId, c.doc_id AS docId, bm25(chunks_fts) AS rank
|
|
195
|
+
FROM chunks_fts JOIN chunks c ON c.chunk_id = chunks_fts.rowid
|
|
196
|
+
WHERE chunks_fts MATCH ?${scopeFilter}
|
|
197
|
+
ORDER BY rank LIMIT ?
|
|
198
|
+
`)
|
|
199
|
+
.all(match, ...(scopes ?? []), max);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const all = run(words.map(quote).join(" "), limit);
|
|
206
|
+
if (all.length >= limit)
|
|
207
|
+
return all;
|
|
208
|
+
// A question rarely shares every word with its answer (T-296 eval: natural questions found
|
|
209
|
+
// nothing). Fill up with any-word matches, bm25-ranked so rarer shared words weigh more;
|
|
210
|
+
// one-letter prefixes would match everything and are left out.
|
|
211
|
+
const anyWord = words.filter((word) => word.length > 1);
|
|
212
|
+
if (anyWord.length < 2)
|
|
213
|
+
return all;
|
|
214
|
+
const seen = new Set(all.map((hit) => hit.chunkId));
|
|
215
|
+
return [
|
|
216
|
+
...all,
|
|
217
|
+
...run(anyWord.map(quote).join(" OR "), limit).filter((hit) => !seen.has(hit.chunkId)),
|
|
218
|
+
].slice(0, limit);
|
|
219
|
+
}
|
|
220
|
+
vecQuery(embedding, limit) {
|
|
221
|
+
if (!this.hasVectors)
|
|
222
|
+
return [];
|
|
223
|
+
const rows = this.db
|
|
224
|
+
.prepare(`
|
|
225
|
+
SELECT v.rowid AS chunkId, c.doc_id AS docId, v.distance AS distance
|
|
226
|
+
FROM chunks_vec v JOIN chunks c ON c.chunk_id = v.rowid
|
|
227
|
+
WHERE v.embedding MATCH ? AND k = ?
|
|
228
|
+
ORDER BY v.distance
|
|
229
|
+
`)
|
|
230
|
+
.all(embedding, limit);
|
|
231
|
+
return rows;
|
|
232
|
+
}
|
|
233
|
+
getDoc(docId) {
|
|
234
|
+
return this.db.prepare("SELECT * FROM docs WHERE doc_id = ?").get(docId);
|
|
235
|
+
}
|
|
236
|
+
getDocByIdOrPath(idOrPath) {
|
|
237
|
+
return this.db
|
|
238
|
+
.prepare("SELECT * FROM docs WHERE id = ? OR path = ?")
|
|
239
|
+
.get(idOrPath, idOrPath);
|
|
240
|
+
}
|
|
241
|
+
getChunk(chunkId) {
|
|
242
|
+
return this.db
|
|
243
|
+
.prepare("SELECT * FROM chunks WHERE chunk_id = ?")
|
|
244
|
+
.get(chunkId);
|
|
245
|
+
}
|
|
246
|
+
linksFor(docId, direction) {
|
|
247
|
+
const rows = (direction === "out"
|
|
248
|
+
? this.db
|
|
249
|
+
.prepare("SELECT * FROM links WHERE source_doc_id = ?")
|
|
250
|
+
.all(docId)
|
|
251
|
+
: this.db
|
|
252
|
+
.prepare("SELECT * FROM links WHERE target_doc_id = ?")
|
|
253
|
+
.all(docId));
|
|
254
|
+
return rows.map((row) => ({
|
|
255
|
+
sourceDocId: row.source_doc_id,
|
|
256
|
+
targetDocId: row.target_doc_id,
|
|
257
|
+
targetRaw: row.target_raw,
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
// Canonical, ordered dump of every row (minus built_at, which changes on every rebuild by
|
|
261
|
+
// design) — used by the round-trip test to prove reindex is deterministic.
|
|
262
|
+
dump() {
|
|
263
|
+
const meta = this.db
|
|
264
|
+
.prepare("SELECT key, value FROM meta WHERE key != 'built_at' ORDER BY key")
|
|
265
|
+
.all();
|
|
266
|
+
const docs = this.db.prepare("SELECT * FROM docs ORDER BY doc_id").all();
|
|
267
|
+
const chunks = this.db
|
|
268
|
+
.prepare("SELECT * FROM chunks ORDER BY chunk_id")
|
|
269
|
+
.all();
|
|
270
|
+
const links = this.db
|
|
271
|
+
.prepare("SELECT * FROM links ORDER BY source_doc_id, target_raw")
|
|
272
|
+
.all();
|
|
273
|
+
return { meta, docs, chunks, links };
|
|
274
|
+
}
|
|
275
|
+
close() {
|
|
276
|
+
this.db.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
export function openIndexReadOnly(file) {
|
|
280
|
+
return new BrainIndexReader(file);
|
|
281
|
+
}
|
|
282
|
+
// The concrete brain-index driver, as the BrainIndexPort — wired into application code only
|
|
283
|
+
// through composition/runtime.ts.
|
|
284
|
+
export const brainIndexPort = { buildIndex, openIndexReadOnly };
|
|
285
|
+
//# sourceMappingURL=brain-index.js.map
|