@amoree-code/ocean 0.0.0-stage → 1.2.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/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/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-hub.js +220 -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,263 @@
|
|
|
1
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { brainIndexPath } from "@ocean/brain/brain/brain-reindex.js";
|
|
4
|
+
import { BrainIndexStaleError, brainSearch, } from "@ocean/brain/brain/brain-service.js";
|
|
5
|
+
import { normalizeScope, scopeVisible, } from "@ocean/core/domain/brain/record-scope.js";
|
|
6
|
+
import { atomicWrite, FRONTMATTER_READ_BYTES, } from "@ocean/core/shared/fs-utils.js";
|
|
7
|
+
import { localDate } from "@ocean/core/shared/local-date.js";
|
|
8
|
+
import { KNOWLEDGE_DIR, oceanRoot, PERSONAL_DIR, resolveWithin, STORE_DIR, } from "@ocean/core/shared/paths.js";
|
|
9
|
+
import { resolveProject } from "@ocean/core/shared/project-resolution.js";
|
|
10
|
+
import { approvalMatchesTarget, operationResult, selectFields, shapeRecords, validateSlug, validateWriteTarget, } from "./operation-contract.js";
|
|
11
|
+
import { freshnessFor, listRecordFiles, readFrontmatterFile, relativeToOcean, requireAbsentTarget, } from "./record-io.js";
|
|
12
|
+
const KNOWLEDGE_KINDS = [
|
|
13
|
+
"architecture",
|
|
14
|
+
"decisions",
|
|
15
|
+
"discoveries",
|
|
16
|
+
"failures",
|
|
17
|
+
"references",
|
|
18
|
+
"research",
|
|
19
|
+
"results",
|
|
20
|
+
"solutions",
|
|
21
|
+
];
|
|
22
|
+
const RECORD_FIELDS = [
|
|
23
|
+
"name",
|
|
24
|
+
"description",
|
|
25
|
+
"id",
|
|
26
|
+
"title",
|
|
27
|
+
"summary",
|
|
28
|
+
"type",
|
|
29
|
+
"status",
|
|
30
|
+
"confidence",
|
|
31
|
+
"updated",
|
|
32
|
+
];
|
|
33
|
+
function provenanceFor(recordType, fields) {
|
|
34
|
+
const declared = (fields.provenance ?? fields.type ?? "").toLowerCase();
|
|
35
|
+
if ([
|
|
36
|
+
"fact",
|
|
37
|
+
"preference",
|
|
38
|
+
"decision",
|
|
39
|
+
"lesson",
|
|
40
|
+
"proposal",
|
|
41
|
+
"temporary-note",
|
|
42
|
+
].includes(declared))
|
|
43
|
+
return declared;
|
|
44
|
+
if (recordType === "task")
|
|
45
|
+
return "task";
|
|
46
|
+
if (recordType === "project")
|
|
47
|
+
return "project";
|
|
48
|
+
if (recordType === "execution")
|
|
49
|
+
return "execution";
|
|
50
|
+
return recordType === "knowledge"
|
|
51
|
+
? "lesson"
|
|
52
|
+
: recordType === "memory"
|
|
53
|
+
? "temporary-note"
|
|
54
|
+
: "unknown";
|
|
55
|
+
}
|
|
56
|
+
function matchesQuery(fields, file, query) {
|
|
57
|
+
if (!query)
|
|
58
|
+
return true;
|
|
59
|
+
const haystack = `${path.basename(file)} ${fields.name ?? fields.id ?? ""} ${fields.description ?? fields.summary ?? ""} ${fields.title ?? ""}`.toLowerCase();
|
|
60
|
+
return haystack.includes(query.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
// Best-effort: the brain index is disposable and may not exist (a fresh checkout, a fixture
|
|
63
|
+
// root in tests, or a corpus that has never been reindexed). Any failure here — missing
|
|
64
|
+
// index, corrupt file — falls back to the substring path below rather than surfacing an
|
|
65
|
+
// error a caller of memory.search/knowledge.search never asked for.
|
|
66
|
+
async function searchViaBrainIndex(recordType, _rootSegments, classification, budget, query, indexPort, project, notes) {
|
|
67
|
+
const root = oceanRoot();
|
|
68
|
+
try {
|
|
69
|
+
await stat(brainIndexPath(root));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// The index stores "<store>/<relative path>"; the store is the record type here.
|
|
75
|
+
const store = recordType === "knowledge" ? "knowledge" : "memory";
|
|
76
|
+
const pathPrefix = store;
|
|
77
|
+
try {
|
|
78
|
+
const result = await brainSearch({
|
|
79
|
+
query,
|
|
80
|
+
root,
|
|
81
|
+
pathPrefix: pathPrefix ? `${pathPrefix}/` : undefined,
|
|
82
|
+
limit: Math.min(20, budget.maxFiles),
|
|
83
|
+
scope: { project },
|
|
84
|
+
indexPort,
|
|
85
|
+
});
|
|
86
|
+
return result.results.map((hit) => ({
|
|
87
|
+
identifier: hit.id,
|
|
88
|
+
recordType,
|
|
89
|
+
provenance: provenanceFor(recordType, { type: hit.type }),
|
|
90
|
+
sourcePath: `${STORE_DIR[store]}/${hit.path.slice(store.length + 1)}`,
|
|
91
|
+
freshness: "unknown",
|
|
92
|
+
confidence: classification.confidence,
|
|
93
|
+
selectionReason: `ranked by brain index (${result.mode})`,
|
|
94
|
+
fields: selectFields({
|
|
95
|
+
id: hit.id,
|
|
96
|
+
title: hit.title,
|
|
97
|
+
summary: hit.summary,
|
|
98
|
+
type: hit.type,
|
|
99
|
+
confidence: hit.confidence,
|
|
100
|
+
updated: hit.lastConfirmedAt ?? "",
|
|
101
|
+
}, RECORD_FIELDS),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// A stale index is the one failure worth naming: the fallback still answers, but the
|
|
106
|
+
// caller should know why ranking is gone and how to get it back.
|
|
107
|
+
if (error instanceof BrainIndexStaleError)
|
|
108
|
+
notes.push(error.message);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export async function searchRecords(operation, recordType, rootSegments, classification, budget, options) {
|
|
113
|
+
let root;
|
|
114
|
+
try {
|
|
115
|
+
root = resolveWithin(oceanRoot(), ...rootSegments);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return operationResult(operation, "record root escapes the Ocean root");
|
|
119
|
+
}
|
|
120
|
+
// Search shows global records plus the cwd's project. An unbound cwd, or a registry that
|
|
121
|
+
// cannot be read, narrows to global — and the failure is reported, not swallowed.
|
|
122
|
+
let project = null;
|
|
123
|
+
const scopeViolations = [];
|
|
124
|
+
try {
|
|
125
|
+
const resolution = await resolveProject(options.cwd ?? process.cwd());
|
|
126
|
+
if (resolution.status === "bound")
|
|
127
|
+
project = resolution.projectId;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
scopeViolations.push(`project unresolved: ${error instanceof Error ? error.message : String(error)}; global records only`);
|
|
131
|
+
}
|
|
132
|
+
if (options.query && options.indexPort) {
|
|
133
|
+
const viaIndex = await searchViaBrainIndex(recordType, rootSegments, classification, budget, options.query, options.indexPort, project, scopeViolations);
|
|
134
|
+
if (viaIndex) {
|
|
135
|
+
const shaped = shapeRecords(viaIndex, budget.maxFiles);
|
|
136
|
+
return {
|
|
137
|
+
operation,
|
|
138
|
+
ok: true,
|
|
139
|
+
reason: `${shaped.records.length} ${recordType} reference(s) selected`,
|
|
140
|
+
records: shaped.records,
|
|
141
|
+
violations: [...scopeViolations, ...shaped.violations],
|
|
142
|
+
written: null,
|
|
143
|
+
packet: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const files = await listRecordFiles(root);
|
|
148
|
+
const records = [];
|
|
149
|
+
const violations = [];
|
|
150
|
+
let bytes = 0;
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
const record = await readFrontmatterFile(file);
|
|
153
|
+
if (!record)
|
|
154
|
+
continue;
|
|
155
|
+
if (!scopeVisible(normalizeScope(record.fields.scope).scope, project))
|
|
156
|
+
continue;
|
|
157
|
+
if (!matchesQuery(record.fields, file, options.query ?? ""))
|
|
158
|
+
continue;
|
|
159
|
+
bytes += Math.min(record.size, FRONTMATTER_READ_BYTES);
|
|
160
|
+
if (bytes > budget.maxBytes) {
|
|
161
|
+
violations.push(`max-bytes-exceeded: stopped searching at ${bytes} bytes (budget ${budget.maxBytes})`);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
records.push({
|
|
165
|
+
identifier: record.fields.name ?? record.fields.id ?? path.basename(file, ".md"),
|
|
166
|
+
recordType,
|
|
167
|
+
provenance: provenanceFor(recordType, record.fields),
|
|
168
|
+
sourcePath: relativeToOcean(file),
|
|
169
|
+
freshness: freshnessFor(record.mtimeMs),
|
|
170
|
+
confidence: classification.confidence,
|
|
171
|
+
selectionReason: options.query
|
|
172
|
+
? `metadata match on query '${options.query}'`
|
|
173
|
+
: `${recordType} record index entry`,
|
|
174
|
+
fields: selectFields(record.fields, RECORD_FIELDS),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const shaped = shapeRecords(records, budget.maxFiles);
|
|
178
|
+
return {
|
|
179
|
+
operation,
|
|
180
|
+
ok: true,
|
|
181
|
+
reason: `${shaped.records.length} ${recordType} reference(s) selected`,
|
|
182
|
+
records: shaped.records,
|
|
183
|
+
violations: [...scopeViolations, ...violations, ...shaped.violations],
|
|
184
|
+
written: null,
|
|
185
|
+
packet: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Canonical brain-record frontmatter (T-228): every new write emits this flat shape
|
|
189
|
+
// directly — id/title/summary/tags/type/confidence/created/updated/last_confirmed_at — plus
|
|
190
|
+
// provenance/status/correction_of, which are Ocean operation metadata, not part of the
|
|
191
|
+
// brain-record schema itself, but useful alongside it. Existing files with the legacy
|
|
192
|
+
// name/description/metadata:{...} shape are read through aliases (brain-markdown.ts) and are
|
|
193
|
+
// never rewritten by this function.
|
|
194
|
+
function recordDocument(slug, recordType, content, provenance, correctionOf) {
|
|
195
|
+
const today = localDate();
|
|
196
|
+
const correction = correctionOf ? `\ncorrection_of: ${correctionOf}` : "";
|
|
197
|
+
return `---\nid: ${slug}\ntitle: ${slug}\nsummary: ""\ntags: []\ntype: ${recordType}\nconfidence: unknown\ncreated: ${today}\nupdated: ${today}\nlast_confirmed_at: ${today}\nprovenance: ${provenance}\nstatus: current${correction}\n---\n\n${content.trim()}\n`;
|
|
198
|
+
}
|
|
199
|
+
export async function writeRecord(operation, options, budget) {
|
|
200
|
+
const slug = validateSlug(options.slug, "record slug");
|
|
201
|
+
if (!slug.valid)
|
|
202
|
+
return operationResult(operation, `write refused: ${slug.reason}`);
|
|
203
|
+
const content = options.content;
|
|
204
|
+
if (typeof content !== "string" || content.trim().length === 0)
|
|
205
|
+
return operationResult(operation, "write refused: no explicit content supplied");
|
|
206
|
+
if (Buffer.byteLength(content, "utf8") > budget.maxBytes) {
|
|
207
|
+
return operationResult(operation, `write refused: content is ${Buffer.byteLength(content, "utf8")} bytes, budget.maxBytes allows ${budget.maxBytes}`, { violations: ["max-bytes-exceeded"] });
|
|
208
|
+
}
|
|
209
|
+
if (options.correctionOf !== undefined &&
|
|
210
|
+
(!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(options.correctionOf) ||
|
|
211
|
+
options.correctionOf.includes(".."))) {
|
|
212
|
+
return operationResult(operation, "write refused: correctionOf must be an explicit safe record identifier");
|
|
213
|
+
}
|
|
214
|
+
let segments;
|
|
215
|
+
let recordType;
|
|
216
|
+
if (operation === "memory.write") {
|
|
217
|
+
segments = [PERSONAL_DIR, `${slug.value}.md`];
|
|
218
|
+
recordType = "memory";
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const kind = validateSlug(options.kind, "knowledge kind");
|
|
222
|
+
if (!kind.valid)
|
|
223
|
+
return operationResult(operation, `write refused: ${kind.reason}`);
|
|
224
|
+
if (!KNOWLEDGE_KINDS.includes(kind.value))
|
|
225
|
+
return operationResult(operation, `write refused: '${kind.value}' is not an allowed knowledge kind`);
|
|
226
|
+
segments = [KNOWLEDGE_DIR, kind.value, `${slug.value}.md`];
|
|
227
|
+
recordType = kind.value === "decisions" ? "decision" : "knowledge";
|
|
228
|
+
}
|
|
229
|
+
const target = validateWriteTarget(...segments);
|
|
230
|
+
if (!target.valid)
|
|
231
|
+
return operationResult(operation, `write refused: ${target.reason}`);
|
|
232
|
+
const approvalCheck = approvalMatchesTarget(options.approval, target.value);
|
|
233
|
+
if (!approvalCheck.ok)
|
|
234
|
+
return operationResult(operation, approvalCheck.reason);
|
|
235
|
+
const absent = await requireAbsentTarget(target.value);
|
|
236
|
+
if (!absent.ok)
|
|
237
|
+
return operationResult(operation, `write refused: ${absent.reason}`);
|
|
238
|
+
try {
|
|
239
|
+
await mkdir(path.dirname(target.value), { recursive: true });
|
|
240
|
+
const provenance = options.provenance ??
|
|
241
|
+
(options.correctionOf
|
|
242
|
+
? "fact"
|
|
243
|
+
: recordType === "memory"
|
|
244
|
+
? "temporary-note"
|
|
245
|
+
: recordType === "decision"
|
|
246
|
+
? "decision"
|
|
247
|
+
: "lesson");
|
|
248
|
+
const bytes = await atomicWrite(target.value, recordDocument(slug.value, recordType, content, provenance, options.correctionOf));
|
|
249
|
+
return {
|
|
250
|
+
operation,
|
|
251
|
+
ok: true,
|
|
252
|
+
reason: `${recordType} record '${slug.value}' written`,
|
|
253
|
+
records: [],
|
|
254
|
+
violations: [],
|
|
255
|
+
written: { sourcePath: relativeToOcean(target.value), bytes },
|
|
256
|
+
packet: null,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return operationResult(operation, `write failed atomically, no partial content left: ${error instanceof Error ? error.message : String(error)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=memory-knowledge-operations.js.map
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { validateBudget } from "@ocean/context/context/context-ladder.js";
|
|
3
|
+
import { TASK_ID } from "@ocean/core/domain/tasks/task-id.js";
|
|
4
|
+
import { engineRoot, oceanRoot, resolveWithin, } from "@ocean/core/shared/paths.js";
|
|
5
|
+
export const OPERATION_KIND = {
|
|
6
|
+
"task.get": "read",
|
|
7
|
+
"task.list": "read",
|
|
8
|
+
"task.update": "write",
|
|
9
|
+
"task.complete": "write",
|
|
10
|
+
"task.create": "write",
|
|
11
|
+
"memory.search": "read",
|
|
12
|
+
"memory.write": "write",
|
|
13
|
+
"knowledge.search": "read",
|
|
14
|
+
"knowledge.write": "write",
|
|
15
|
+
"project.detect": "read",
|
|
16
|
+
"project.create": "write",
|
|
17
|
+
"project.update": "write",
|
|
18
|
+
};
|
|
19
|
+
export function operationResult(operation, reason, overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
operation,
|
|
22
|
+
ok: false,
|
|
23
|
+
reason,
|
|
24
|
+
records: [],
|
|
25
|
+
violations: [],
|
|
26
|
+
written: null,
|
|
27
|
+
packet: null,
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// --- identifier and path validation -------------------------------------------------
|
|
32
|
+
// Record slugs and project names: conservative allow-list, no dots that could build "..",
|
|
33
|
+
// no separators, no shell syntax, no whitespace.
|
|
34
|
+
const SLUG_SHAPE = /^[a-z0-9][a-z0-9-]{0,63}$/i;
|
|
35
|
+
export function validateTaskIdentifier(identifier) {
|
|
36
|
+
if (typeof identifier !== "string" || identifier.length === 0)
|
|
37
|
+
return { valid: false, reason: "task identifier is missing" };
|
|
38
|
+
if (identifier.includes("\0"))
|
|
39
|
+
return { valid: false, reason: "task identifier contains a null byte" };
|
|
40
|
+
if (!TASK_ID.test(identifier))
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
reason: `task identifier '${identifier}' does not match the required <PREFIX>-<digits> shape, expected <PREFIX>-<digits>`,
|
|
44
|
+
};
|
|
45
|
+
return { valid: true, value: identifier };
|
|
46
|
+
}
|
|
47
|
+
export function validateSlug(slug, label) {
|
|
48
|
+
if (typeof slug !== "string" || slug.length === 0)
|
|
49
|
+
return { valid: false, reason: `${label} is missing` };
|
|
50
|
+
if (slug.includes("\0"))
|
|
51
|
+
return { valid: false, reason: `${label} contains a null byte` };
|
|
52
|
+
if (path.isAbsolute(slug))
|
|
53
|
+
return { valid: false, reason: `${label} must not be an absolute path` };
|
|
54
|
+
if (slug.includes(".."))
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
reason: `${label} must not contain a traversal segment`,
|
|
58
|
+
};
|
|
59
|
+
if (/[;&|`$()<>\\/\s]/.test(slug))
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
reason: `${label} contains an unsafe or separator character`,
|
|
63
|
+
};
|
|
64
|
+
if (!SLUG_SHAPE.test(slug))
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
reason: `${label} '${slug}' does not match the allowed shape`,
|
|
68
|
+
};
|
|
69
|
+
return { valid: true, value: slug };
|
|
70
|
+
}
|
|
71
|
+
// A write destination must resolve inside the private Ocean workspace root and must never
|
|
72
|
+
// land inside the public engine package, whatever the caller passed.
|
|
73
|
+
export function validateWriteTarget(...segments) {
|
|
74
|
+
let resolved;
|
|
75
|
+
try {
|
|
76
|
+
resolved = resolveWithin(oceanRoot(), ...segments);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { valid: false, reason: "write target escapes the Ocean root" };
|
|
80
|
+
}
|
|
81
|
+
const engine = path.resolve(engineRoot());
|
|
82
|
+
if (resolved === engine || resolved.startsWith(`${engine}${path.sep}`)) {
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
reason: "refusing to write private Ocean content inside the public engine package",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { valid: true, value: resolved };
|
|
89
|
+
}
|
|
90
|
+
// --- operation/intent validation ----------------------------------------------------
|
|
91
|
+
// The single source of truth for which classified intent may drive which operation. An
|
|
92
|
+
// operation requested for an intent that does not map to it is refused, not coerced.
|
|
93
|
+
export function operationForIntent(classification) {
|
|
94
|
+
const { intent, action, identifier } = classification;
|
|
95
|
+
if (intent === "task-lookup") {
|
|
96
|
+
if (action === "complete")
|
|
97
|
+
return { operation: "task.complete" };
|
|
98
|
+
if (action === "update")
|
|
99
|
+
return { operation: "task.update" };
|
|
100
|
+
if (identifier)
|
|
101
|
+
return { operation: "task.get" };
|
|
102
|
+
return { operation: "task.list" };
|
|
103
|
+
}
|
|
104
|
+
if (intent === "task-create")
|
|
105
|
+
return { operation: "task.create" };
|
|
106
|
+
if (intent === "memory-lookup")
|
|
107
|
+
return { operation: "memory.search" };
|
|
108
|
+
if (intent === "work-style-lookup")
|
|
109
|
+
return { operation: "memory.search" };
|
|
110
|
+
if (intent === "knowledge-lookup")
|
|
111
|
+
return { operation: "knowledge.search" };
|
|
112
|
+
if (intent === "decision-lookup")
|
|
113
|
+
return { operation: "knowledge.search" };
|
|
114
|
+
if (intent === "project-detect")
|
|
115
|
+
return { operation: "project.detect" };
|
|
116
|
+
if (intent === "project-create")
|
|
117
|
+
return { operation: "project.create" };
|
|
118
|
+
if (intent === "remember") {
|
|
119
|
+
if (classification.entityType === "knowledge" ||
|
|
120
|
+
classification.entityType === "decision")
|
|
121
|
+
return { operation: "knowledge.write" };
|
|
122
|
+
return { operation: "memory.write" };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
operation: null,
|
|
126
|
+
reason: `intent '${intent}' does not map to any Ocean record operation`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function assertOperationMatchesIntent(operation, classification) {
|
|
130
|
+
const mapped = operationForIntent(classification);
|
|
131
|
+
if (!mapped.operation)
|
|
132
|
+
return { ok: false, reason: mapped.reason };
|
|
133
|
+
if (mapped.operation !== operation) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: `operation '${operation}' does not match intent '${classification.intent}' (which maps to '${mapped.operation}')`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { ok: true };
|
|
140
|
+
}
|
|
141
|
+
// Writes are fail-closed: only a high-confidence, unambiguous classification, with a valid
|
|
142
|
+
// budget and an approval naming this exact operation and target, may proceed.
|
|
143
|
+
export function gateOperation(operation, classification, budget, approval) {
|
|
144
|
+
const intentCheck = assertOperationMatchesIntent(operation, classification);
|
|
145
|
+
if (!intentCheck.ok)
|
|
146
|
+
return intentCheck;
|
|
147
|
+
const budgetCheck = validateBudget(budget);
|
|
148
|
+
if (!budgetCheck.valid)
|
|
149
|
+
return { ok: false, reason: `invalid-budget: ${budgetCheck.reason}` };
|
|
150
|
+
if (classification.intent === "unknown")
|
|
151
|
+
return { ok: false, reason: "unknown intent — failing closed" };
|
|
152
|
+
if (classification.ambiguityReason)
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
reason: `ambiguous intent — failing closed: ${classification.ambiguityReason}`,
|
|
156
|
+
};
|
|
157
|
+
if (OPERATION_KIND[operation] === "read")
|
|
158
|
+
return { ok: true };
|
|
159
|
+
if (classification.confidence !== "high") {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
reason: `write refused: intent confidence is '${classification.confidence}', only 'high' may write`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (approval?.approved !== true)
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: "write refused: no explicit approval was supplied",
|
|
169
|
+
};
|
|
170
|
+
if (approval.operation !== operation)
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
reason: `write refused: approval was granted for '${approval.operation}', not '${operation}'`,
|
|
174
|
+
};
|
|
175
|
+
return { ok: true };
|
|
176
|
+
}
|
|
177
|
+
export function approvalMatchesTarget(approval, resolvedTarget) {
|
|
178
|
+
const approvedTarget = path.resolve(approval.target);
|
|
179
|
+
if (approvedTarget !== path.resolve(resolvedTarget)) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: `write refused: approval target '${approval.target}' does not match the resolved destination`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { ok: true };
|
|
186
|
+
}
|
|
187
|
+
// --- bounded result shaping -----------------------------------------------------------
|
|
188
|
+
// Stable order (by identifier), deterministic deduplication (by identifier + sourcePath),
|
|
189
|
+
// and a hard result cap taken from the caller's budget.
|
|
190
|
+
export function shapeRecords(records, maxRecords) {
|
|
191
|
+
const violations = [];
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
const unique = [];
|
|
194
|
+
for (const record of records) {
|
|
195
|
+
const key = `${record.identifier}::${record.sourcePath}`;
|
|
196
|
+
if (seen.has(key)) {
|
|
197
|
+
violations.push(`duplicate record skipped: ${key}`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
seen.add(key);
|
|
201
|
+
unique.push(record);
|
|
202
|
+
}
|
|
203
|
+
unique.sort((left, right) => left.identifier.localeCompare(right.identifier) ||
|
|
204
|
+
left.sourcePath.localeCompare(right.sourcePath));
|
|
205
|
+
if (unique.length > maxRecords) {
|
|
206
|
+
violations.push(`result truncated to maxFiles=${maxRecords} (${unique.length} candidates matched)`);
|
|
207
|
+
return { records: unique.slice(0, maxRecords), violations };
|
|
208
|
+
}
|
|
209
|
+
return { records: unique, violations };
|
|
210
|
+
}
|
|
211
|
+
// Field selection: only the named fields survive into a record, and each value is clipped
|
|
212
|
+
// so one oversized frontmatter value cannot make a result unbounded.
|
|
213
|
+
const MAX_FIELD_CHARS = 200;
|
|
214
|
+
export function selectFields(source, fields) {
|
|
215
|
+
const selected = {};
|
|
216
|
+
for (const field of fields) {
|
|
217
|
+
const value = source[field];
|
|
218
|
+
if (value === undefined)
|
|
219
|
+
continue;
|
|
220
|
+
selected[field] =
|
|
221
|
+
value.length > MAX_FIELD_CHARS
|
|
222
|
+
? `${value.slice(0, MAX_FIELD_CHARS)}…`
|
|
223
|
+
: value;
|
|
224
|
+
}
|
|
225
|
+
return selected;
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=operation-contract.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { oceanPath, projectDir, sameProject, } from "@ocean/core/shared/paths.js";
|
|
2
|
+
import { resolveProject } from "@ocean/core/shared/project-resolution.js";
|
|
3
|
+
async function activeProjectId(cwd) {
|
|
4
|
+
const resolution = await resolveProject(cwd);
|
|
5
|
+
if (resolution.status === "bound")
|
|
6
|
+
return { ok: true, projectId: resolution.projectId };
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
reason: `no bound Ocean project for this working directory (status: ${resolution.status})`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function taskRoot(projectId) {
|
|
13
|
+
return oceanPath(projectDir(projectId), "tasks");
|
|
14
|
+
}
|
|
15
|
+
export async function crossProjectGuard(options, cwd) {
|
|
16
|
+
const active = await activeProjectId(cwd);
|
|
17
|
+
if (!active.ok)
|
|
18
|
+
return active;
|
|
19
|
+
if (options.requestedProject &&
|
|
20
|
+
!sameProject(options.requestedProject, active.projectId)) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: `cross-project request refused: active project is '${active.projectId}', request targeted '${options.requestedProject}'`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return active;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=operation-scope.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { oceanPath, PROJECTS_DIR, projectDir, projectSubdir, } from "@ocean/core/shared/paths.js";
|
|
4
|
+
import { bindProject, projectConfirmationQuestion, resolveProject, } from "@ocean/core/shared/project-resolution.js";
|
|
5
|
+
import { approvalMatchesTarget, operationResult, validateSlug, validateWriteTarget, } from "./operation-contract.js";
|
|
6
|
+
import { crossProjectGuard } from "./operation-scope.js";
|
|
7
|
+
import { relativeToOcean } from "./record-io.js";
|
|
8
|
+
export async function projectDetect(classification, cwd) {
|
|
9
|
+
const resolution = await resolveProject(cwd);
|
|
10
|
+
const record = {
|
|
11
|
+
identifier: resolution.status === "bound" ? resolution.projectId : "unbound",
|
|
12
|
+
recordType: "project",
|
|
13
|
+
provenance: "project",
|
|
14
|
+
sourcePath: resolution.status === "bound"
|
|
15
|
+
? relativeToOcean(oceanPath(projectDir(resolution.projectId)))
|
|
16
|
+
: "",
|
|
17
|
+
freshness: "unknown",
|
|
18
|
+
confidence: classification.confidence,
|
|
19
|
+
selectionReason: `project resolution status '${resolution.status}' for the given working directory`,
|
|
20
|
+
fields: { status: resolution.status, confidence: resolution.confidence },
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
operation: "project.detect",
|
|
24
|
+
ok: true,
|
|
25
|
+
reason: `project resolution: ${resolution.status}`,
|
|
26
|
+
records: [record],
|
|
27
|
+
violations: [],
|
|
28
|
+
written: null,
|
|
29
|
+
packet: null,
|
|
30
|
+
confirmationQuestion: projectConfirmationQuestion(resolution),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function projectCreate(classification, options) {
|
|
34
|
+
const name = validateSlug(classification.identifier, "project name");
|
|
35
|
+
if (!name.valid)
|
|
36
|
+
return operationResult("project.create", `write refused: ${name.reason}`);
|
|
37
|
+
const projectPath = options.projectPath;
|
|
38
|
+
if (typeof projectPath !== "string" || !path.isAbsolute(projectPath)) {
|
|
39
|
+
return operationResult("project.create", "write refused: an explicit absolute project path is required");
|
|
40
|
+
}
|
|
41
|
+
if (projectPath.includes("\0"))
|
|
42
|
+
return operationResult("project.create", "write refused: project path contains a null byte");
|
|
43
|
+
const target = validateWriteTarget(PROJECTS_DIR, projectSubdir(oceanPath(PROJECTS_DIR), name.value));
|
|
44
|
+
if (!target.valid)
|
|
45
|
+
return operationResult("project.create", `write refused: ${target.reason}`);
|
|
46
|
+
const approvalCheck = approvalMatchesTarget(options.approval, target.value);
|
|
47
|
+
if (!approvalCheck.ok)
|
|
48
|
+
return operationResult("project.create", approvalCheck.reason);
|
|
49
|
+
const binding = await bindProject(name.value, projectPath);
|
|
50
|
+
if (binding.conflict) {
|
|
51
|
+
return operationResult("project.create", `write refused: '${name.value}' conflicts with an existing binding at ${binding.conflict.path}`);
|
|
52
|
+
}
|
|
53
|
+
await mkdir(target.value, { recursive: true });
|
|
54
|
+
return {
|
|
55
|
+
operation: "project.create",
|
|
56
|
+
ok: true,
|
|
57
|
+
reason: binding.created
|
|
58
|
+
? `project '${name.value}' created and bound to ${projectPath}`
|
|
59
|
+
: `project '${name.value}' already bound to ${projectPath}`,
|
|
60
|
+
records: [],
|
|
61
|
+
violations: [],
|
|
62
|
+
written: { sourcePath: relativeToOcean(target.value), bytes: 0 },
|
|
63
|
+
packet: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function projectUpdate(_classification, options, cwd) {
|
|
67
|
+
const project = await crossProjectGuard(options, cwd);
|
|
68
|
+
if (!project.ok)
|
|
69
|
+
return operationResult("project.update", project.reason);
|
|
70
|
+
const projectPath = options.projectPath;
|
|
71
|
+
if (typeof projectPath !== "string" || !path.isAbsolute(projectPath)) {
|
|
72
|
+
return operationResult("project.update", "write refused: an explicit absolute project path is required");
|
|
73
|
+
}
|
|
74
|
+
const target = validateWriteTarget(PROJECTS_DIR, projectSubdir(oceanPath(PROJECTS_DIR), project.projectId));
|
|
75
|
+
if (!target.valid)
|
|
76
|
+
return operationResult("project.update", `write refused: ${target.reason}`);
|
|
77
|
+
const approvalCheck = approvalMatchesTarget(options.approval, target.value);
|
|
78
|
+
if (!approvalCheck.ok)
|
|
79
|
+
return operationResult("project.update", approvalCheck.reason);
|
|
80
|
+
const binding = await bindProject(project.projectId, projectPath);
|
|
81
|
+
if (binding.conflict)
|
|
82
|
+
return operationResult("project.update", `write refused: binding conflict for '${project.projectId}'`);
|
|
83
|
+
return {
|
|
84
|
+
operation: "project.update",
|
|
85
|
+
ok: true,
|
|
86
|
+
reason: `project '${project.projectId}' binding confirmed`,
|
|
87
|
+
records: [],
|
|
88
|
+
violations: [],
|
|
89
|
+
written: { sourcePath: relativeToOcean(target.value), bytes: 0 },
|
|
90
|
+
packet: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=project-operations.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FRONTMATTER_READ_BYTES, readFrontmatterFile as readFrontmatterFileWithLimit, } from "@ocean/core/shared/fs-utils.js";
|
|
4
|
+
import { oceanRoot } from "@ocean/core/shared/paths.js";
|
|
5
|
+
// Bounded, deterministic Ocean record I/O (T-198 slice 7). Every read is scoped to a
|
|
6
|
+
// specific record directory and capped by the caller's budget — there is no repository-wide
|
|
7
|
+
// scan anywhere in this module. Every write is atomic (temp file + rename, see fs-utils.ts)
|
|
8
|
+
// and refuses to run without an explicit, matching approval.
|
|
9
|
+
const STALE_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
|
|
10
|
+
// Hard structural caps, independent of the caller's budget: a directory walk can never
|
|
11
|
+
// examine more than this, whatever budget is passed.
|
|
12
|
+
const MAX_SCAN_ENTRIES = 200;
|
|
13
|
+
const MAX_SCAN_DEPTH = 3;
|
|
14
|
+
export function freshnessFor(mtimeMs) {
|
|
15
|
+
return Date.now() - mtimeMs <= STALE_AFTER_MS ? "current" : "stale";
|
|
16
|
+
}
|
|
17
|
+
export async function readFrontmatterFile(file) {
|
|
18
|
+
return readFrontmatterFileWithLimit(file, FRONTMATTER_READ_BYTES);
|
|
19
|
+
}
|
|
20
|
+
// Depth-limited, entry-capped listing of one record directory. Never recurses outside the
|
|
21
|
+
// directory it was given and never returns more than MAX_SCAN_ENTRIES paths.
|
|
22
|
+
export async function listRecordFiles(root, depth = 0, budgetLeft = { entries: MAX_SCAN_ENTRIES }) {
|
|
23
|
+
if (depth > MAX_SCAN_DEPTH || budgetLeft.entries <= 0)
|
|
24
|
+
return [];
|
|
25
|
+
let entries;
|
|
26
|
+
try {
|
|
27
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
34
|
+
if (budgetLeft.entries <= 0)
|
|
35
|
+
break;
|
|
36
|
+
if (entry.name.startsWith("."))
|
|
37
|
+
continue;
|
|
38
|
+
const full = path.join(root, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
files.push(...(await listRecordFiles(full, depth + 1, budgetLeft)));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!entry.name.endsWith(".md"))
|
|
44
|
+
continue;
|
|
45
|
+
budgetLeft.entries -= 1;
|
|
46
|
+
files.push(full);
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
export function relativeToOcean(file) {
|
|
51
|
+
return path.relative(oceanRoot(), file).split(path.sep).join("/");
|
|
52
|
+
}
|
|
53
|
+
export async function requireAbsentTarget(target) {
|
|
54
|
+
try {
|
|
55
|
+
await stat(target);
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: `refusing to overwrite an existing record at ${relativeToOcean(target)}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=record-io.js.map
|