@jamesaphoenix/tx-core 0.8.0 → 0.9.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/dist/db.d.ts +6 -6
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +29 -2
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/cycle-scan-service-impl.d.ts +28 -0
- package/dist/internal/cycle-scan-service-impl.d.ts.map +1 -0
- package/dist/internal/cycle-scan-service-impl.js +877 -0
- package/dist/internal/cycle-scan-service-impl.js.map +1 -0
- package/dist/internal/doc-service-impl.d.ts +42 -0
- package/dist/internal/doc-service-impl.d.ts.map +1 -0
- package/dist/internal/doc-service-impl.js +812 -0
- package/dist/internal/doc-service-impl.js.map +1 -0
- package/dist/internal/embedding-service-impl.d.ts +202 -0
- package/dist/internal/embedding-service-impl.d.ts.map +1 -0
- package/dist/internal/embedding-service-impl.js +466 -0
- package/dist/internal/embedding-service-impl.js.map +1 -0
- package/dist/internal/memory-service-impl.d.ts +49 -0
- package/dist/internal/memory-service-impl.d.ts.map +1 -0
- package/dist/internal/memory-service-impl.js +1061 -0
- package/dist/internal/memory-service-impl.js.map +1 -0
- package/dist/internal/spec-trace-service-impl.d.ts +50 -0
- package/dist/internal/spec-trace-service-impl.d.ts.map +1 -0
- package/dist/internal/spec-trace-service-impl.js +707 -0
- package/dist/internal/spec-trace-service-impl.js.map +1 -0
- package/dist/internal/sync/service-impl.d.ts +41 -0
- package/dist/internal/sync/service-impl.d.ts.map +1 -0
- package/dist/{services/sync-service.js → internal/sync/service-impl.js} +954 -499
- package/dist/internal/sync/service-impl.js.map +1 -0
- package/dist/layer.d.ts +8 -5
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +22 -10
- package/dist/layer.js.map +1 -1
- package/dist/mappers/anchor.d.ts.map +1 -1
- package/dist/mappers/anchor.js +5 -4
- package/dist/mappers/anchor.js.map +1 -1
- package/dist/mappers/attempt.d.ts.map +1 -1
- package/dist/mappers/attempt.js +2 -1
- package/dist/mappers/attempt.js.map +1 -1
- package/dist/mappers/candidate.d.ts.map +1 -1
- package/dist/mappers/candidate.js +2 -1
- package/dist/mappers/candidate.js.map +1 -1
- package/dist/mappers/claim.d.ts.map +1 -1
- package/dist/mappers/claim.js +2 -1
- package/dist/mappers/claim.js.map +1 -1
- package/dist/mappers/doc.d.ts.map +1 -1
- package/dist/mappers/doc.js +9 -8
- package/dist/mappers/doc.js.map +1 -1
- package/dist/mappers/edge.d.ts.map +1 -1
- package/dist/mappers/edge.js +4 -3
- package/dist/mappers/edge.js.map +1 -1
- package/dist/mappers/file-learning.d.ts.map +1 -1
- package/dist/mappers/file-learning.js +2 -1
- package/dist/mappers/file-learning.js.map +1 -1
- package/dist/mappers/index.d.ts +1 -0
- package/dist/mappers/index.d.ts.map +1 -1
- package/dist/mappers/index.js +2 -0
- package/dist/mappers/index.js.map +1 -1
- package/dist/mappers/learning.d.ts.map +1 -1
- package/dist/mappers/learning.js +4 -3
- package/dist/mappers/learning.js.map +1 -1
- package/dist/mappers/memory.d.ts.map +1 -1
- package/dist/mappers/memory.js +7 -6
- package/dist/mappers/memory.js.map +1 -1
- package/dist/mappers/orchestrator-state.d.ts.map +1 -1
- package/dist/mappers/orchestrator-state.js +2 -1
- package/dist/mappers/orchestrator-state.js.map +1 -1
- package/dist/mappers/pin.d.ts.map +1 -1
- package/dist/mappers/pin.js +2 -1
- package/dist/mappers/pin.js.map +1 -1
- package/dist/mappers/run.d.ts.map +1 -1
- package/dist/mappers/run.js +4 -3
- package/dist/mappers/run.js.map +1 -1
- package/dist/mappers/spec-trace.d.ts +11 -0
- package/dist/mappers/spec-trace.d.ts.map +1 -0
- package/dist/mappers/spec-trace.js +59 -0
- package/dist/mappers/spec-trace.js.map +1 -0
- package/dist/mappers/task.d.ts.map +1 -1
- package/dist/mappers/task.js +4 -4
- package/dist/mappers/task.js.map +1 -1
- package/dist/mappers/worker.d.ts.map +1 -1
- package/dist/mappers/worker.js +2 -1
- package/dist/mappers/worker.js.map +1 -1
- package/dist/migrations-embedded.d.ts.map +1 -1
- package/dist/migrations-embedded.js +15 -0
- package/dist/migrations-embedded.js.map +1 -1
- package/dist/repo/anchor-repo.d.ts +2 -2
- package/dist/repo/anchor-repo.d.ts.map +1 -1
- package/dist/repo/anchor-repo.js +18 -17
- package/dist/repo/anchor-repo.js.map +1 -1
- package/dist/repo/attempt-repo.d.ts.map +1 -1
- package/dist/repo/attempt-repo.js +11 -10
- package/dist/repo/attempt-repo.js.map +1 -1
- package/dist/repo/candidate-repo.d.ts.map +1 -1
- package/dist/repo/candidate-repo.js +8 -7
- package/dist/repo/candidate-repo.js.map +1 -1
- package/dist/repo/claim-repo.d.ts +4 -4
- package/dist/repo/claim-repo.d.ts.map +1 -1
- package/dist/repo/claim-repo.js +14 -13
- package/dist/repo/claim-repo.js.map +1 -1
- package/dist/repo/compaction-repo.d.ts +4 -4
- package/dist/repo/compaction-repo.d.ts.map +1 -1
- package/dist/repo/compaction-repo.js +7 -6
- package/dist/repo/compaction-repo.js.map +1 -1
- package/dist/repo/deduplication-repo.d.ts.map +1 -1
- package/dist/repo/deduplication-repo.js +80 -57
- package/dist/repo/deduplication-repo.js.map +1 -1
- package/dist/repo/dep-repo.d.ts.map +1 -1
- package/dist/repo/dep-repo.js +90 -88
- package/dist/repo/dep-repo.js.map +1 -1
- package/dist/repo/doc-repo.d.ts +2 -63
- package/dist/repo/doc-repo.d.ts.map +1 -1
- package/dist/repo/doc-repo.js +6 -4
- package/dist/repo/doc-repo.js.map +1 -1
- package/dist/repo/doc-repo.types.d.ts +64 -0
- package/dist/repo/doc-repo.types.d.ts.map +1 -0
- package/dist/repo/doc-repo.types.js +2 -0
- package/dist/repo/doc-repo.types.js.map +1 -0
- package/dist/repo/edge-repo.d.ts.map +1 -1
- package/dist/repo/edge-repo.js +91 -81
- package/dist/repo/edge-repo.js.map +1 -1
- package/dist/repo/file-learning-repo.d.ts.map +1 -1
- package/dist/repo/file-learning-repo.js +7 -6
- package/dist/repo/file-learning-repo.js.map +1 -1
- package/dist/repo/guard-repo.d.ts +4 -4
- package/dist/repo/guard-repo.d.ts.map +1 -1
- package/dist/repo/guard-repo.js +8 -7
- package/dist/repo/guard-repo.js.map +1 -1
- package/dist/repo/index.d.ts +1 -0
- package/dist/repo/index.d.ts.map +1 -1
- package/dist/repo/index.js +1 -0
- package/dist/repo/index.js.map +1 -1
- package/dist/repo/label-repo.d.ts +4 -4
- package/dist/repo/label-repo.d.ts.map +1 -1
- package/dist/repo/label-repo.js +9 -8
- package/dist/repo/label-repo.js.map +1 -1
- package/dist/repo/learning-repo.d.ts +2 -2
- package/dist/repo/learning-repo.d.ts.map +1 -1
- package/dist/repo/learning-repo.js +37 -31
- package/dist/repo/learning-repo.js.map +1 -1
- package/dist/repo/memory-repo/document.d.ts +4 -0
- package/dist/repo/memory-repo/document.d.ts.map +1 -0
- package/dist/repo/memory-repo/document.js +205 -0
- package/dist/repo/memory-repo/document.js.map +1 -0
- package/dist/repo/memory-repo/link.d.ts +4 -0
- package/dist/repo/memory-repo/link.d.ts.map +1 -0
- package/dist/repo/memory-repo/link.js +124 -0
- package/dist/repo/memory-repo/link.js.map +1 -0
- package/dist/repo/memory-repo/property.d.ts +4 -0
- package/dist/repo/memory-repo/property.d.ts.map +1 -0
- package/dist/repo/memory-repo/property.js +56 -0
- package/dist/repo/memory-repo/property.js.map +1 -0
- package/dist/repo/memory-repo/shared.d.ts +28 -0
- package/dist/repo/memory-repo/shared.d.ts.map +1 -0
- package/dist/repo/memory-repo/shared.js +63 -0
- package/dist/repo/memory-repo/shared.js.map +1 -0
- package/dist/repo/memory-repo/source.d.ts +4 -0
- package/dist/repo/memory-repo/source.d.ts.map +1 -0
- package/dist/repo/memory-repo/source.js +58 -0
- package/dist/repo/memory-repo/source.js.map +1 -0
- package/dist/repo/memory-repo.d.ts +14 -10
- package/dist/repo/memory-repo.d.ts.map +1 -1
- package/dist/repo/memory-repo.js +8 -527
- package/dist/repo/memory-repo.js.map +1 -1
- package/dist/repo/message-repo.d.ts.map +1 -1
- package/dist/repo/message-repo.js +9 -8
- package/dist/repo/message-repo.js.map +1 -1
- package/dist/repo/orchestrator-state-repo.d.ts.map +1 -1
- package/dist/repo/orchestrator-state-repo.js +2 -1
- package/dist/repo/orchestrator-state-repo.js.map +1 -1
- package/dist/repo/pin-repo.d.ts.map +1 -1
- package/dist/repo/pin-repo.js +5 -4
- package/dist/repo/pin-repo.js.map +1 -1
- package/dist/repo/run-repo.d.ts.map +1 -1
- package/dist/repo/run-repo.js +11 -10
- package/dist/repo/run-repo.js.map +1 -1
- package/dist/repo/spec-trace-repo.d.ts +9 -0
- package/dist/repo/spec-trace-repo.d.ts.map +1 -0
- package/dist/repo/spec-trace-repo.filter.d.ts +3 -0
- package/dist/repo/spec-trace-repo.filter.d.ts.map +1 -0
- package/dist/repo/spec-trace-repo.filter.js +13 -0
- package/dist/repo/spec-trace-repo.filter.js.map +1 -0
- package/dist/repo/spec-trace-repo.js +323 -0
- package/dist/repo/spec-trace-repo.js.map +1 -0
- package/dist/repo/spec-trace-repo.types.d.ts +60 -0
- package/dist/repo/spec-trace-repo.types.d.ts.map +1 -0
- package/dist/repo/spec-trace-repo.types.js +2 -0
- package/dist/repo/spec-trace-repo.types.js.map +1 -0
- package/dist/repo/task-repo/factory.d.ts +4 -0
- package/dist/repo/task-repo/factory.d.ts.map +1 -0
- package/dist/repo/task-repo/factory.js +7 -0
- package/dist/repo/task-repo/factory.js.map +1 -0
- package/dist/repo/task-repo/read.d.ts +6 -0
- package/dist/repo/task-repo/read.d.ts.map +1 -0
- package/dist/repo/task-repo/read.js +332 -0
- package/dist/repo/task-repo/read.js.map +1 -0
- package/dist/repo/task-repo/shared.d.ts +10 -0
- package/dist/repo/task-repo/shared.d.ts.map +1 -0
- package/dist/repo/task-repo/shared.js +29 -0
- package/dist/repo/task-repo/shared.js.map +1 -0
- package/dist/repo/task-repo/write.d.ts +6 -0
- package/dist/repo/task-repo/write.d.ts.map +1 -0
- package/dist/repo/task-repo/write.js +182 -0
- package/dist/repo/task-repo/write.js.map +1 -0
- package/dist/repo/task-repo.d.ts +5 -4
- package/dist/repo/task-repo.d.ts.map +1 -1
- package/dist/repo/task-repo.js +2 -520
- package/dist/repo/task-repo.js.map +1 -1
- package/dist/repo/tracked-project-repo.d.ts.map +1 -1
- package/dist/repo/tracked-project-repo.js +6 -5
- package/dist/repo/tracked-project-repo.js.map +1 -1
- package/dist/repo/worker-repo.d.ts.map +1 -1
- package/dist/repo/worker-repo.js +60 -47
- package/dist/repo/worker-repo.js.map +1 -1
- package/dist/schemas/index.d.ts +4 -2
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/sync-events.d.ts +25 -0
- package/dist/schemas/sync-events.d.ts.map +1 -0
- package/dist/schemas/sync-events.js +23 -0
- package/dist/schemas/sync-events.js.map +1 -0
- package/dist/schemas/sync.d.ts +20 -10
- package/dist/schemas/sync.d.ts.map +1 -1
- package/dist/schemas/sync.js +10 -4
- package/dist/schemas/sync.js.map +1 -1
- package/dist/services/agent-service.d.ts +4 -4
- package/dist/services/agent-service.d.ts.map +1 -1
- package/dist/services/agent-service.js.map +1 -1
- package/dist/services/anchor/anchor-service-core-ops.d.ts +125 -0
- package/dist/services/anchor/anchor-service-core-ops.d.ts.map +1 -0
- package/dist/services/anchor/anchor-service-core-ops.js +41 -0
- package/dist/services/anchor/anchor-service-core-ops.js.map +1 -0
- package/dist/services/anchor/anchor-service-deps.d.ts +10 -0
- package/dist/services/anchor/anchor-service-deps.d.ts.map +1 -0
- package/dist/services/anchor/anchor-service-deps.js +2 -0
- package/dist/services/anchor/anchor-service-deps.js.map +1 -0
- package/dist/services/anchor/anchor-service-ops.d.ts +296 -0
- package/dist/services/anchor/anchor-service-ops.d.ts.map +1 -0
- package/dist/services/anchor/anchor-service-ops.js +9 -0
- package/dist/services/anchor/anchor-service-ops.js.map +1 -0
- package/dist/services/anchor/anchor-service-state-ops.d.ts +116 -0
- package/dist/services/anchor/anchor-service-state-ops.d.ts.map +1 -0
- package/dist/services/anchor/anchor-service-state-ops.js +150 -0
- package/dist/services/anchor/anchor-service-state-ops.js.map +1 -0
- package/dist/services/anchor/anchor-service-validation.d.ts +7 -0
- package/dist/services/anchor/anchor-service-validation.d.ts.map +1 -0
- package/dist/services/anchor/anchor-service-validation.js +114 -0
- package/dist/services/anchor/anchor-service-validation.js.map +1 -0
- package/dist/services/anchor/anchor-service-verification-ops.d.ts +71 -0
- package/dist/services/anchor/anchor-service-verification-ops.d.ts.map +1 -0
- package/dist/services/anchor/anchor-service-verification-ops.js +169 -0
- package/dist/services/anchor/anchor-service-verification-ops.js.map +1 -0
- package/dist/services/anchor/anchor-verification-batch.d.ts +12 -0
- package/dist/services/anchor/anchor-verification-batch.d.ts.map +1 -0
- package/dist/services/anchor/anchor-verification-batch.js +109 -0
- package/dist/services/anchor/anchor-verification-batch.js.map +1 -0
- package/dist/services/anchor/anchor-verification-single.d.ts +7 -0
- package/dist/services/anchor/anchor-verification-single.d.ts.map +1 -0
- package/dist/services/anchor/anchor-verification-single.js +407 -0
- package/dist/services/anchor/anchor-verification-single.js.map +1 -0
- package/dist/services/anchor/anchor-verification-utils.d.ts +19 -0
- package/dist/services/anchor/anchor-verification-utils.d.ts.map +1 -0
- package/dist/services/anchor/anchor-verification-utils.js +107 -0
- package/dist/services/anchor/anchor-verification-utils.js.map +1 -0
- package/dist/services/anchor-service.d.ts +12 -90
- package/dist/services/anchor-service.d.ts.map +1 -1
- package/dist/services/anchor-service.js +5 -530
- package/dist/services/anchor-service.js.map +1 -1
- package/dist/services/anchor-verification.d.ts +9 -60
- package/dist/services/anchor-verification.d.ts.map +1 -1
- package/dist/services/anchor-verification.js +5 -796
- package/dist/services/anchor-verification.js.map +1 -1
- package/dist/services/ast-grep-service/patterns.d.ts +90 -0
- package/dist/services/ast-grep-service/patterns.d.ts.map +1 -0
- package/dist/services/ast-grep-service/patterns.js +261 -0
- package/dist/services/ast-grep-service/patterns.js.map +1 -0
- package/dist/services/ast-grep-service.d.ts +2 -13
- package/dist/services/ast-grep-service.d.ts.map +1 -1
- package/dist/services/ast-grep-service.js +3 -261
- package/dist/services/ast-grep-service.js.map +1 -1
- package/dist/services/auto-sync-service.d.ts +3 -3
- package/dist/services/auto-sync-service.d.ts.map +1 -1
- package/dist/services/auto-sync-service.js +12 -13
- package/dist/services/auto-sync-service.js.map +1 -1
- package/dist/services/compaction-service.d.ts +6 -6
- package/dist/services/compaction-service.d.ts.map +1 -1
- package/dist/services/compaction-service.js +11 -7
- package/dist/services/compaction-service.js.map +1 -1
- package/dist/services/cycle-scan-service.d.ts +1 -27
- package/dist/services/cycle-scan-service.d.ts.map +1 -1
- package/dist/services/cycle-scan-service.js +1 -876
- package/dist/services/cycle-scan-service.js.map +1 -1
- package/dist/services/daemon-service/process.d.ts +93 -0
- package/dist/services/daemon-service/process.d.ts.map +1 -0
- package/dist/services/daemon-service/process.js +325 -0
- package/dist/services/daemon-service/process.js.map +1 -0
- package/dist/services/daemon-service/templates.d.ts +88 -0
- package/dist/services/daemon-service/templates.d.ts.map +1 -0
- package/dist/services/daemon-service/templates.js +119 -0
- package/dist/services/daemon-service/templates.js.map +1 -0
- package/dist/services/daemon-service.d.ts +8 -160
- package/dist/services/daemon-service.d.ts.map +1 -1
- package/dist/services/daemon-service.js +4 -440
- package/dist/services/daemon-service.js.map +1 -1
- package/dist/services/doc-service.d.ts +1 -41
- package/dist/services/doc-service.d.ts.map +1 -1
- package/dist/services/doc-service.js +1 -780
- package/dist/services/doc-service.js.map +1 -1
- package/dist/services/edge-service.d.ts +6 -6
- package/dist/services/edge-service.d.ts.map +1 -1
- package/dist/services/edge-service.js.map +1 -1
- package/dist/services/embedding-service.d.ts +1 -201
- package/dist/services/embedding-service.d.ts.map +1 -1
- package/dist/services/embedding-service.js +1 -465
- package/dist/services/embedding-service.js.map +1 -1
- package/dist/services/feedback-tracker.d.ts +2 -2
- package/dist/services/feedback-tracker.d.ts.map +1 -1
- package/dist/services/feedback-tracker.js.map +1 -1
- package/dist/services/file-watcher-service/shared.d.ts +76 -0
- package/dist/services/file-watcher-service/shared.d.ts.map +1 -0
- package/dist/services/file-watcher-service/shared.js +43 -0
- package/dist/services/file-watcher-service/shared.js.map +1 -0
- package/dist/services/file-watcher-service.d.ts +2 -42
- package/dist/services/file-watcher-service.d.ts.map +1 -1
- package/dist/services/file-watcher-service.js +1 -46
- package/dist/services/file-watcher-service.js.map +1 -1
- package/dist/services/graph-expansion/from-files.d.ts +14 -0
- package/dist/services/graph-expansion/from-files.d.ts.map +1 -0
- package/dist/services/graph-expansion/from-files.js +187 -0
- package/dist/services/graph-expansion/from-files.js.map +1 -0
- package/dist/services/graph-expansion/live.d.ts +11 -0
- package/dist/services/graph-expansion/live.d.ts.map +1 -0
- package/dist/services/graph-expansion/live.js +263 -0
- package/dist/services/graph-expansion/live.js.map +1 -0
- package/dist/services/graph-expansion.d.ts +17 -20
- package/dist/services/graph-expansion.d.ts.map +1 -1
- package/dist/services/graph-expansion.js +2 -439
- package/dist/services/graph-expansion.js.map +1 -1
- package/dist/services/guard-service.d.ts +2 -2
- package/dist/services/guard-service.d.ts.map +1 -1
- package/dist/services/guard-service.js.map +1 -1
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/learning-service.d.ts +4 -4
- package/dist/services/learning-service.d.ts.map +1 -1
- package/dist/services/learning-service.js.map +1 -1
- package/dist/services/llm-service.d.ts +4 -4
- package/dist/services/llm-service.d.ts.map +1 -1
- package/dist/services/llm-service.js.map +1 -1
- package/dist/services/memory-retriever-service.d.ts.map +1 -1
- package/dist/services/memory-retriever-service.js.map +1 -1
- package/dist/services/memory-service.d.ts +1 -48
- package/dist/services/memory-service.d.ts.map +1 -1
- package/dist/services/memory-service.js +1 -1060
- package/dist/services/memory-service.js.map +1 -1
- package/dist/services/migration-service.d.ts +6 -6
- package/dist/services/migration-service.d.ts.map +1 -1
- package/dist/services/migration-service.js +6 -2
- package/dist/services/migration-service.js.map +1 -1
- package/dist/services/orchestrator-service.d.ts +2 -2
- package/dist/services/orchestrator-service.d.ts.map +1 -1
- package/dist/services/orchestrator-service.js.map +1 -1
- package/dist/services/pin-service.d.ts.map +1 -1
- package/dist/services/pin-service.js +21 -28
- package/dist/services/pin-service.js.map +1 -1
- package/dist/services/promotion-service.d.ts +4 -4
- package/dist/services/promotion-service.d.ts.map +1 -1
- package/dist/services/promotion-service.js.map +1 -1
- package/dist/services/query-expansion-service.d.ts +2 -2
- package/dist/services/query-expansion-service.d.ts.map +1 -1
- package/dist/services/query-expansion-service.js.map +1 -1
- package/dist/services/reflect-service.d.ts +6 -6
- package/dist/services/reflect-service.d.ts.map +1 -1
- package/dist/services/reflect-service.js.map +1 -1
- package/dist/services/reranker-service.d.ts +2 -2
- package/dist/services/reranker-service.d.ts.map +1 -1
- package/dist/services/reranker-service.js.map +1 -1
- package/dist/services/retriever-scoring.d.ts +52 -0
- package/dist/services/retriever-scoring.d.ts.map +1 -0
- package/dist/services/retriever-scoring.js +146 -0
- package/dist/services/retriever-scoring.js.map +1 -0
- package/dist/services/retriever-service.d.ts.map +1 -1
- package/dist/services/retriever-service.js +1 -191
- package/dist/services/retriever-service.js.map +1 -1
- package/dist/services/run-heartbeat-service.d.ts +10 -10
- package/dist/services/run-heartbeat-service.d.ts.map +1 -1
- package/dist/services/run-heartbeat-service.js.map +1 -1
- package/dist/services/score-service.d.ts +2 -2
- package/dist/services/score-service.d.ts.map +1 -1
- package/dist/services/score-service.js.map +1 -1
- package/dist/services/spec-trace-service.d.ts +2 -0
- package/dist/services/spec-trace-service.d.ts.map +1 -0
- package/dist/services/spec-trace-service.js +2 -0
- package/dist/services/spec-trace-service.js.map +1 -0
- package/dist/services/stream-service.d.ts +31 -0
- package/dist/services/stream-service.d.ts.map +1 -0
- package/dist/services/stream-service.js +162 -0
- package/dist/services/stream-service.js.map +1 -0
- package/dist/services/swarm-verification/shared.d.ts +71 -0
- package/dist/services/swarm-verification/shared.d.ts.map +1 -0
- package/dist/services/swarm-verification/shared.js +108 -0
- package/dist/services/swarm-verification/shared.js.map +1 -0
- package/dist/services/swarm-verification.d.ts +4 -68
- package/dist/services/swarm-verification.d.ts.map +1 -1
- package/dist/services/swarm-verification.js +2 -122
- package/dist/services/swarm-verification.js.map +1 -1
- package/dist/services/sync/converters.d.ts +63 -0
- package/dist/services/sync/converters.d.ts.map +1 -0
- package/dist/services/sync/converters.js +253 -0
- package/dist/services/sync/converters.js.map +1 -0
- package/dist/services/sync/entity-export.d.ts +22 -0
- package/dist/services/sync/entity-export.d.ts.map +1 -0
- package/dist/services/sync/entity-export.js +15 -0
- package/dist/services/sync/entity-export.js.map +1 -0
- package/dist/services/sync/entity-import.d.ts +22 -0
- package/dist/services/sync/entity-import.d.ts.map +1 -0
- package/dist/services/sync/entity-import.js +15 -0
- package/dist/services/sync/entity-import.js.map +1 -0
- package/dist/services/sync/file-utils.d.ts +23 -0
- package/dist/services/sync/file-utils.d.ts.map +1 -0
- package/dist/services/sync/file-utils.js +97 -0
- package/dist/services/sync/file-utils.js.map +1 -0
- package/dist/services/sync/index.d.ts +3 -0
- package/dist/services/sync/index.d.ts.map +1 -0
- package/dist/services/sync/index.js +2 -0
- package/dist/services/sync/index.js.map +1 -0
- package/dist/services/sync/service.d.ts +2 -0
- package/dist/services/sync/service.d.ts.map +1 -0
- package/dist/services/sync/service.js +2 -0
- package/dist/services/sync/service.js.map +1 -0
- package/dist/services/sync/sync-helpers.d.ts +54 -0
- package/dist/services/sync/sync-helpers.d.ts.map +1 -0
- package/dist/services/sync/sync-helpers.js +245 -0
- package/dist/services/sync/sync-helpers.js.map +1 -0
- package/dist/services/sync/types.d.ts +104 -0
- package/dist/services/sync/types.d.ts.map +1 -0
- package/dist/services/sync/types.js +5 -0
- package/dist/services/sync/types.js.map +1 -0
- package/dist/services/task-service/internals.d.ts +36 -0
- package/dist/services/task-service/internals.d.ts.map +1 -0
- package/dist/services/task-service/internals.js +270 -0
- package/dist/services/task-service/internals.js.map +1 -0
- package/dist/services/task-service.d.ts +2 -1
- package/dist/services/task-service.d.ts.map +1 -1
- package/dist/services/task-service.js +28 -236
- package/dist/services/task-service.js.map +1 -1
- package/dist/services/tracing-service.d.ts +2 -2
- package/dist/services/tracing-service.d.ts.map +1 -1
- package/dist/services/tracing-service.js.map +1 -1
- package/dist/services/transcript-adapter.d.ts +6 -6
- package/dist/services/transcript-adapter.d.ts.map +1 -1
- package/dist/services/transcript-adapter.js +3 -8
- package/dist/services/transcript-adapter.js.map +1 -1
- package/dist/services/validation-service.d.ts +8 -8
- package/dist/services/validation-service.d.ts.map +1 -1
- package/dist/services/validation-service.js.map +1 -1
- package/dist/services/verify-service.d.ts +2 -2
- package/dist/services/verify-service.d.ts.map +1 -1
- package/dist/services/verify-service.js.map +1 -1
- package/dist/services/worker-process/runtime.d.ts +51 -0
- package/dist/services/worker-process/runtime.d.ts.map +1 -0
- package/dist/services/worker-process/runtime.js +150 -0
- package/dist/services/worker-process/runtime.js.map +1 -0
- package/dist/services/worker-process.d.ts +3 -18
- package/dist/services/worker-process.d.ts.map +1 -1
- package/dist/services/worker-process.js +3 -173
- package/dist/services/worker-process.js.map +1 -1
- package/dist/services/worker-service.d.ts +6 -6
- package/dist/services/worker-service.d.ts.map +1 -1
- package/dist/services/worker-service.js.map +1 -1
- package/dist/utils/db-result.d.ts +14 -0
- package/dist/utils/db-result.d.ts.map +1 -0
- package/dist/utils/db-result.js +37 -0
- package/dist/utils/db-result.js.map +1 -0
- package/dist/utils/doc-renderer.d.ts +10 -10
- package/dist/utils/doc-renderer.d.ts.map +1 -1
- package/dist/utils/doc-renderer.js.map +1 -1
- package/dist/utils/ears-validator.d.ts +2 -2
- package/dist/utils/ears-validator.d.ts.map +1 -1
- package/dist/utils/ears-validator.js.map +1 -1
- package/dist/utils/file-path.d.ts +27 -0
- package/dist/utils/file-path.d.ts.map +1 -0
- package/dist/utils/file-path.js +77 -0
- package/dist/utils/file-path.js.map +1 -0
- package/dist/utils/glob.d.ts +2 -11
- package/dist/utils/glob.d.ts.map +1 -1
- package/dist/utils/glob.js +22 -13
- package/dist/utils/glob.js.map +1 -1
- package/dist/utils/spec-discovery.d.ts +34 -0
- package/dist/utils/spec-discovery.d.ts.map +1 -0
- package/dist/utils/spec-discovery.js +344 -0
- package/dist/utils/spec-discovery.js.map +1 -0
- package/dist/utils/toml-config.d.ts +7 -2
- package/dist/utils/toml-config.d.ts.map +1 -1
- package/dist/utils/toml-config.js +106 -2
- package/dist/utils/toml-config.js.map +1 -1
- package/dist/utils/ulid.d.ts +8 -0
- package/dist/utils/ulid.d.ts.map +1 -0
- package/dist/utils/ulid.js +30 -0
- package/dist/utils/ulid.js.map +1 -0
- package/dist/worker/hooks.d.ts +10 -10
- package/dist/worker/hooks.d.ts.map +1 -1
- package/dist/worker/run-worker.d.ts.map +1 -1
- package/dist/worker/run-worker.js.map +1 -1
- package/migrations/033_sync_events.sql +33 -0
- package/migrations/034_spec_test_traceability.sql +51 -0
- package/migrations/035_anchor_schema_repair.sql +82 -0
- package/package.json +3 -2
- package/dist/services/sync-service.d.ts +0 -247
- package/dist/services/sync-service.d.ts.map +0 -1
- package/dist/services/sync-service.js.map +0 -1
|
@@ -1,1061 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* MemoryService - Core service for filesystem-backed memory
|
|
3
|
-
*
|
|
4
|
-
* Manages indexing, searching, and CRUD operations on markdown files.
|
|
5
|
-
* Filesystem is the source of truth; SQLite is a derived index.
|
|
6
|
-
*/
|
|
7
|
-
import { Context, Effect, Layer } from "effect";
|
|
8
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
9
|
-
import { readFile, writeFile, readdir, stat, mkdir, rename, unlink } from "node:fs/promises";
|
|
10
|
-
import { join, relative, basename, extname, resolve } from "node:path";
|
|
11
|
-
import { MemoryDocumentRepository } from "../repo/memory-repo.js";
|
|
12
|
-
import { MemoryLinkRepository } from "../repo/memory-repo.js";
|
|
13
|
-
import { MemoryPropertyRepository } from "../repo/memory-repo.js";
|
|
14
|
-
import { MemorySourceRepository } from "../repo/memory-repo.js";
|
|
15
|
-
import { DatabaseError, MemoryDocumentNotFoundError, MemorySourceNotFoundError, ValidationError } from "../errors.js";
|
|
16
|
-
// Reserved frontmatter keys that are NOT synced as properties
|
|
17
|
-
const RESERVED_FRONTMATTER_KEYS = new Set(["tags", "related", "created"]);
|
|
18
|
-
/** Max recursion depth for findMarkdownFiles to prevent symlink cycles / stack overflow. */
|
|
19
|
-
const MAX_DIRECTORY_DEPTH = 50;
|
|
20
|
-
/**
|
|
21
|
-
* Generate a deterministic memory document ID from the relative file path.
|
|
22
|
-
* Uses 12 hex chars (48 bits) — birthday collision threshold at ~4M documents.
|
|
23
|
-
* (Prior 8-char version had ~50% collision probability at only ~65K documents.)
|
|
24
|
-
*/
|
|
25
|
-
const generateDocId = (relativePath, rootDir) => {
|
|
26
|
-
const hash = createHash("sha256").update(`${rootDir}:${relativePath}`).digest("hex").slice(0, 12);
|
|
27
|
-
return `mem-${hash}`;
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Slugify a title for use as a filename.
|
|
31
|
-
*/
|
|
32
|
-
const slugify = (title) => {
|
|
33
|
-
return title
|
|
34
|
-
.toLowerCase()
|
|
35
|
-
.replace(/[^\p{L}\p{N}\s-]/gu, "") // Unicode-aware: keep letters, numbers, whitespace, hyphens
|
|
36
|
-
.replace(/\s+/g, "-")
|
|
37
|
-
.replace(/-+/g, "-")
|
|
38
|
-
.replace(/^-|-$/g, "")
|
|
39
|
-
.slice(0, 80);
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* Quote a YAML value if it contains characters that would break parsing.
|
|
43
|
-
*/
|
|
44
|
-
/** YAML reserved words that must be quoted to preserve string type */
|
|
45
|
-
const YAML_RESERVED_WORDS = new Set([
|
|
46
|
-
"null", "Null", "NULL", "~",
|
|
47
|
-
"true", "True", "TRUE", "false", "False", "FALSE",
|
|
48
|
-
"yes", "Yes", "YES", "no", "No", "NO",
|
|
49
|
-
"on", "On", "ON", "off", "Off", "OFF",
|
|
50
|
-
]);
|
|
51
|
-
const yamlQuote = (value) => {
|
|
52
|
-
if (YAML_RESERVED_WORDS.has(value) ||
|
|
53
|
-
/^[-+]?\d/.test(value) ||
|
|
54
|
-
/[:#[\]{}|>&*!'"?%@`,\n\r\t\0]/.test(value) ||
|
|
55
|
-
value.startsWith(" ") || value.endsWith(" ") || value === "") {
|
|
56
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\0/g, "\\0")}"`;
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Quote a YAML array item if it contains commas or special characters.
|
|
62
|
-
* Reuses YAML_RESERVED_WORDS from above for boolean/null interop safety.
|
|
63
|
-
*/
|
|
64
|
-
const yamlQuoteItem = (item) => {
|
|
65
|
-
if (item.includes(",") || item.includes('"') || item.includes("'") ||
|
|
66
|
-
item.includes("[") || item.includes("]") ||
|
|
67
|
-
item.includes("{") || item.includes("}") ||
|
|
68
|
-
item.includes("|") || item.includes(">") ||
|
|
69
|
-
item.includes("&") || item.includes("*") ||
|
|
70
|
-
item.includes("!") || item.includes("?") ||
|
|
71
|
-
item.includes("%") || item.includes("@") || item.includes("`") ||
|
|
72
|
-
item.includes("\n") || item.includes("\r") || item.includes("\t") || item.includes("\0") ||
|
|
73
|
-
item.includes("\\") || item === "" ||
|
|
74
|
-
item.includes(":") || item.includes("#") ||
|
|
75
|
-
item.startsWith(" ") || item.endsWith(" ") ||
|
|
76
|
-
YAML_RESERVED_WORDS.has(item) ||
|
|
77
|
-
/^[-+]?[0-9]/.test(item)) {
|
|
78
|
-
return `"${item.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\0/g, "\\0")}"`;
|
|
79
|
-
}
|
|
80
|
-
return item;
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* Parse YAML frontmatter from markdown content.
|
|
84
|
-
* Returns { frontmatter, body, parsed } where frontmatter is the raw YAML string,
|
|
85
|
-
* body is the remaining content, and parsed is the structured object.
|
|
86
|
-
*/
|
|
87
|
-
const parseFrontmatter = (content) => {
|
|
88
|
-
// Strip UTF-8 BOM if present (editors like VS Code can add this)
|
|
89
|
-
const cleaned = content.startsWith("\uFEFF") ? content.slice(1) : content;
|
|
90
|
-
// Must start with --- on the first line
|
|
91
|
-
if (!cleaned.startsWith("---\n") && !cleaned.startsWith("---\r\n")) {
|
|
92
|
-
return { frontmatter: null, body: content, parsed: null };
|
|
93
|
-
}
|
|
94
|
-
// Find the closing --- delimiter: must be on its own line immediately after the opening.
|
|
95
|
-
// Uses a line-by-line scan instead of a lazy regex to avoid truncating body content
|
|
96
|
-
// at in-body horizontal rules (---).
|
|
97
|
-
const openLen = cleaned.startsWith("---\r\n") ? 5 : 4;
|
|
98
|
-
const rest = cleaned.slice(openLen);
|
|
99
|
-
const restLines = rest.split(/\r?\n/);
|
|
100
|
-
let closingIdx = -1;
|
|
101
|
-
let charsConsumed = 0;
|
|
102
|
-
for (let i = 0; i < restLines.length; i++) {
|
|
103
|
-
if (restLines[i].trimEnd() === "---") {
|
|
104
|
-
closingIdx = i;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
charsConsumed += restLines[i].length + (rest[charsConsumed + restLines[i].length] === "\r" ? 2 : 1);
|
|
108
|
-
}
|
|
109
|
-
if (closingIdx === -1)
|
|
110
|
-
return { frontmatter: null, body: content, parsed: null };
|
|
111
|
-
const yamlStr = restLines.slice(0, closingIdx).join("\n");
|
|
112
|
-
// Body starts after the closing --- and its line ending
|
|
113
|
-
const closingLineLen = restLines[closingIdx].length;
|
|
114
|
-
const closingEnd = charsConsumed + closingLineLen;
|
|
115
|
-
// Skip the newline after closing --- (if present)
|
|
116
|
-
let bodyStart = openLen + closingEnd;
|
|
117
|
-
if (cleaned[bodyStart] === "\r")
|
|
118
|
-
bodyStart++;
|
|
119
|
-
if (cleaned[bodyStart] === "\n")
|
|
120
|
-
bodyStart++;
|
|
121
|
-
const body = cleaned.slice(bodyStart);
|
|
122
|
-
// Simple YAML parser for flat key-value + arrays + block scalars
|
|
123
|
-
const parsed = {};
|
|
124
|
-
const lines = yamlStr.split(/\r?\n/);
|
|
125
|
-
let currentKey = null;
|
|
126
|
-
// Block scalar tracking (|, >, |-, >-, |+, >+)
|
|
127
|
-
let currentBlockKey = null;
|
|
128
|
-
let currentBlockIndent = -1;
|
|
129
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
130
|
-
const line = lines[lineIdx];
|
|
131
|
-
// Block scalar continuation: collect indented or empty lines
|
|
132
|
-
if (currentBlockKey !== null) {
|
|
133
|
-
const indentMatch = line.match(/^(\s+)/);
|
|
134
|
-
if (indentMatch || line.trim() === "") {
|
|
135
|
-
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
136
|
-
if (currentBlockIndent < 0 && indent > 0)
|
|
137
|
-
currentBlockIndent = indent;
|
|
138
|
-
const stripped = currentBlockIndent > 0 ? line.slice(currentBlockIndent) : line;
|
|
139
|
-
const existing = parsed[currentBlockKey];
|
|
140
|
-
parsed[currentBlockKey] = existing ? existing + "\n" + stripped : stripped;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
// Non-indented line ends the block scalar — fall through to normal parsing
|
|
145
|
-
currentBlockKey = null;
|
|
146
|
-
currentBlockIndent = -1;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const kvMatch = line.match(/^(\w[\w.-]*)\s*:\s*(.*)$/);
|
|
150
|
-
if (kvMatch) {
|
|
151
|
-
const key = kvMatch[1];
|
|
152
|
-
let value = kvMatch[2].trim();
|
|
153
|
-
// Strip inline YAML comments from unquoted values (e.g., "active # was draft" → "active")
|
|
154
|
-
if (typeof value === "string" && !value.startsWith('"') && !value.startsWith("'") && !value.startsWith("[")) {
|
|
155
|
-
value = value.replace(/\s+#.*$/, "");
|
|
156
|
-
}
|
|
157
|
-
// Handle inline array: [a, b, c]
|
|
158
|
-
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
|
|
159
|
-
const inner = value.slice(1, -1);
|
|
160
|
-
if (inner.trim() === "") {
|
|
161
|
-
value = [];
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
// Parse items, respecting quoted strings and escape sequences
|
|
165
|
-
const items = [];
|
|
166
|
-
let current = "";
|
|
167
|
-
let inQuotes = false;
|
|
168
|
-
let quoteChar = "";
|
|
169
|
-
let wasQuoted = false;
|
|
170
|
-
for (let i = 0; i < inner.length; i++) {
|
|
171
|
-
const ch = inner[i];
|
|
172
|
-
// Handle escape sequences inside double-quoted strings
|
|
173
|
-
if (inQuotes && quoteChar === '"' && ch === "\\" && i + 1 < inner.length) {
|
|
174
|
-
const next = inner[i + 1];
|
|
175
|
-
switch (next) {
|
|
176
|
-
case "n":
|
|
177
|
-
current += "\n";
|
|
178
|
-
break;
|
|
179
|
-
case "r":
|
|
180
|
-
current += "\r";
|
|
181
|
-
break;
|
|
182
|
-
case "t":
|
|
183
|
-
current += "\t";
|
|
184
|
-
break;
|
|
185
|
-
case "0":
|
|
186
|
-
current += "\0";
|
|
187
|
-
break;
|
|
188
|
-
case '"':
|
|
189
|
-
current += '"';
|
|
190
|
-
break;
|
|
191
|
-
case "\\":
|
|
192
|
-
current += "\\";
|
|
193
|
-
break;
|
|
194
|
-
default:
|
|
195
|
-
current += ch + next;
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
i++; // skip next character
|
|
199
|
-
}
|
|
200
|
-
else if (!inQuotes && (ch === '"' || ch === "'")) {
|
|
201
|
-
inQuotes = true;
|
|
202
|
-
quoteChar = ch;
|
|
203
|
-
wasQuoted = true;
|
|
204
|
-
// Discard inter-item whitespace before the opening quote
|
|
205
|
-
// e.g., in `[a, "b"]` the space before `"b"` is separator, not value content
|
|
206
|
-
// Only clear if current is pure whitespace (no real content accumulated)
|
|
207
|
-
if (current.trim() === "")
|
|
208
|
-
current = "";
|
|
209
|
-
}
|
|
210
|
-
else if (inQuotes && ch === quoteChar) {
|
|
211
|
-
// Handle '' escape inside single-quoted strings (YAML spec: '' → ')
|
|
212
|
-
if (quoteChar === "'" && i + 1 < inner.length && inner[i + 1] === "'") {
|
|
213
|
-
current += "'";
|
|
214
|
-
i++; // skip the second quote
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
inQuotes = false;
|
|
218
|
-
}
|
|
219
|
-
else if (!inQuotes && ch === ",") {
|
|
220
|
-
// Push item: preserve empty strings that were explicitly quoted (e.g. "")
|
|
221
|
-
// When quoted, preserve exact content (don't trim spaces inside quotes)
|
|
222
|
-
if (wasQuoted) {
|
|
223
|
-
items.push({ text: current, quoted: true });
|
|
224
|
-
}
|
|
225
|
-
else if (current.trim()) {
|
|
226
|
-
items.push({ text: current.trim(), quoted: false });
|
|
227
|
-
}
|
|
228
|
-
current = "";
|
|
229
|
-
wasQuoted = false;
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// Don't accumulate post-quote whitespace (separator, not content)
|
|
233
|
-
// e.g., ["hello" , "world"] — the space after closing quote is not part of the value
|
|
234
|
-
if (wasQuoted && !inQuotes && (ch === " " || ch === "\t")) {
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
// Reset wasQuoted if non-whitespace appears after closing quote (malformed input)
|
|
238
|
-
// This prevents corrupting the quoted/unquoted type metadata for coercion
|
|
239
|
-
if (wasQuoted && !inQuotes) {
|
|
240
|
-
wasQuoted = false;
|
|
241
|
-
}
|
|
242
|
-
current += ch;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
// Push final item: preserve empty strings that were explicitly quoted
|
|
246
|
-
if (wasQuoted) {
|
|
247
|
-
items.push({ text: current, quoted: true });
|
|
248
|
-
}
|
|
249
|
-
else if (current.trim()) {
|
|
250
|
-
items.push({ text: current.trim(), quoted: false });
|
|
251
|
-
}
|
|
252
|
-
// Coerce ONLY unquoted array items to native types (same rules as scalar values).
|
|
253
|
-
// Quoted items (e.g., "42", "true") are kept as strings — quoting is intentional.
|
|
254
|
-
value = items.map((item) => {
|
|
255
|
-
if (item.quoted)
|
|
256
|
-
return item.text;
|
|
257
|
-
const t = item.text;
|
|
258
|
-
if (/^(null|Null|NULL|~)$/.test(t))
|
|
259
|
-
return null;
|
|
260
|
-
if (/^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF)$/.test(t)) {
|
|
261
|
-
const lower = t.toLowerCase();
|
|
262
|
-
return (lower === "true" || lower === "yes" || lower === "on");
|
|
263
|
-
}
|
|
264
|
-
if (/^-?\d+$/.test(t))
|
|
265
|
-
return parseInt(t, 10);
|
|
266
|
-
if (/^-?\d+\.\d+$/.test(t))
|
|
267
|
-
return parseFloat(t);
|
|
268
|
-
return t;
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// Handle quoted string value (decode escape sequences for double-quoted strings)
|
|
273
|
-
// Order matters: protect literal backslashes first to avoid double-decode
|
|
274
|
-
// e.g. "\\n" (literal backslash + n) must not become a newline
|
|
275
|
-
else if (typeof value === "string" && value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
276
|
-
// Single-pass decode: match all escape sequences at once
|
|
277
|
-
value = value.slice(1, -1).replace(/\\([\\"nrt0])/g, (_match, ch) => {
|
|
278
|
-
switch (ch) {
|
|
279
|
-
case "n": return "\n";
|
|
280
|
-
case "r": return "\r";
|
|
281
|
-
case "t": return "\t";
|
|
282
|
-
case "0": return "\0";
|
|
283
|
-
case '"': return '"';
|
|
284
|
-
case "\\": return "\\";
|
|
285
|
-
default: return ch;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
// Handle single-quoted string value (no escape processing per YAML spec, but '' → ' unescape)
|
|
290
|
-
else if (typeof value === "string" && value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
291
|
-
value = value.slice(1, -1).replace(/''/g, "'");
|
|
292
|
-
}
|
|
293
|
-
// Handle bare YAML null — preserve native type so round-trip doesn't quote as "null"
|
|
294
|
-
else if (typeof value === "string" && /^(null|Null|NULL|~)$/.test(value)) {
|
|
295
|
-
value = null;
|
|
296
|
-
}
|
|
297
|
-
// Handle bare YAML booleans — preserve native type so round-trip doesn't quote them
|
|
298
|
-
else if (typeof value === "string" && /^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF)$/.test(value)) {
|
|
299
|
-
const lower = value.toLowerCase();
|
|
300
|
-
value = (lower === "true" || lower === "yes" || lower === "on");
|
|
301
|
-
}
|
|
302
|
-
// Handle bare integers — preserve native type so round-trip doesn't quote them
|
|
303
|
-
else if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
304
|
-
value = parseInt(value, 10);
|
|
305
|
-
}
|
|
306
|
-
// Handle bare floats — preserve native type
|
|
307
|
-
else if (typeof value === "string" && /^-?\d+\.\d+$/.test(value)) {
|
|
308
|
-
value = parseFloat(value);
|
|
309
|
-
}
|
|
310
|
-
// Handle YAML block scalar indicators (|, >, |-, >-, |+, >+)
|
|
311
|
-
else if (typeof value === "string" && /^[|>][+-]?$/.test(value.trim())) {
|
|
312
|
-
currentBlockKey = key;
|
|
313
|
-
currentBlockIndent = -1;
|
|
314
|
-
parsed[key] = "";
|
|
315
|
-
currentKey = null;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
// Handle empty value: could be a block array (items follow) or bare null.
|
|
319
|
-
// Peek at the next non-blank line to decide: if it starts with "- ", treat as array.
|
|
320
|
-
// Otherwise treat as null (YAML spec: bare `key:` with no value is null).
|
|
321
|
-
else if (value === "") {
|
|
322
|
-
// Peek ahead: find next non-blank, non-comment line
|
|
323
|
-
let nextLine;
|
|
324
|
-
for (let j = lineIdx + 1; j < lines.length; j++) {
|
|
325
|
-
const trimmed = lines[j].trim();
|
|
326
|
-
if (trimmed !== "" && !trimmed.startsWith("#")) {
|
|
327
|
-
nextLine = lines[j];
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (nextLine && /^\s*-(\s|$)/.test(nextLine)) {
|
|
332
|
-
// Next content line is a block array item → treat as array
|
|
333
|
-
value = [];
|
|
334
|
-
currentKey = key;
|
|
335
|
-
parsed[key] = value;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
// No array items follow → bare null value
|
|
339
|
-
value = null;
|
|
340
|
-
parsed[key] = value;
|
|
341
|
-
currentKey = null;
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
parsed[key] = value;
|
|
345
|
-
currentKey = null;
|
|
346
|
-
}
|
|
347
|
-
else if (currentKey && line.match(/^\s*-\s+(.+)$/)) {
|
|
348
|
-
// Array item
|
|
349
|
-
const itemMatch = line.match(/^\s*-\s+(.+)$/);
|
|
350
|
-
if (itemMatch && Array.isArray(parsed[currentKey])) {
|
|
351
|
-
let itemVal = itemMatch[1].trim();
|
|
352
|
-
// Only strip MATCHED quote pairs (not unmatched leading/trailing quotes)
|
|
353
|
-
if (typeof itemVal === "string" && itemVal.length >= 2) {
|
|
354
|
-
if ((itemVal.startsWith('"') && itemVal.endsWith('"')) ||
|
|
355
|
-
(itemVal.startsWith("'") && itemVal.endsWith("'"))) {
|
|
356
|
-
const wasDoubleQuoted = itemVal.startsWith('"');
|
|
357
|
-
itemVal = itemVal.slice(1, -1);
|
|
358
|
-
// Decode escape sequences for double-quoted items
|
|
359
|
-
if (wasDoubleQuoted) {
|
|
360
|
-
itemVal = itemVal.replace(/\\([\\"nrt0])/g, (_m, ch) => {
|
|
361
|
-
switch (ch) {
|
|
362
|
-
case "n": return "\n";
|
|
363
|
-
case "r": return "\r";
|
|
364
|
-
case "t": return "\t";
|
|
365
|
-
case "0": return "\0";
|
|
366
|
-
case '"': return '"';
|
|
367
|
-
case "\\": return "\\";
|
|
368
|
-
default: return ch;
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
// Single-quoted: '' → ' unescape
|
|
374
|
-
itemVal = itemVal.replace(/''/g, "'");
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
// Type-coerce unquoted block array items (consistent with inline arrays)
|
|
379
|
-
if (typeof itemVal === "string") {
|
|
380
|
-
const t = itemVal;
|
|
381
|
-
const isQuoted = itemMatch[1].trim().startsWith('"') || itemMatch[1].trim().startsWith("'");
|
|
382
|
-
if (!isQuoted) {
|
|
383
|
-
if (/^(null|Null|NULL|~)$/.test(t))
|
|
384
|
-
itemVal = null;
|
|
385
|
-
else if (/^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF)$/.test(t)) {
|
|
386
|
-
const lower = t.toLowerCase();
|
|
387
|
-
itemVal = (lower === "true" || lower === "yes" || lower === "on");
|
|
388
|
-
}
|
|
389
|
-
else if (/^-?\d+$/.test(t))
|
|
390
|
-
itemVal = parseInt(t, 10);
|
|
391
|
-
else if (/^-?\d+\.\d+$/.test(t))
|
|
392
|
-
itemVal = parseFloat(t);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
;
|
|
396
|
-
parsed[currentKey].push(itemVal);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
else if (currentKey && line.match(/^\s*-\s*$/)) {
|
|
400
|
-
// Empty array item (bare "- " or "-") → null (YAML spec: empty sequence entry is null)
|
|
401
|
-
if (Array.isArray(parsed[currentKey])) {
|
|
402
|
-
;
|
|
403
|
-
parsed[currentKey].push(null);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return { frontmatter: yamlStr, body, parsed };
|
|
408
|
-
};
|
|
409
|
-
/**
|
|
410
|
-
* Extract the title from markdown content: first H1 heading or filename.
|
|
411
|
-
*/
|
|
412
|
-
const extractTitle = (content, filename) => {
|
|
413
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
414
|
-
if (match) {
|
|
415
|
-
return match[1].trim()
|
|
416
|
-
// Strip wikilinks: [[page|alias]] → alias, [[page]] → page
|
|
417
|
-
.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, "$2")
|
|
418
|
-
.replace(/\[\[([^\]]+)\]\]/g, "$1")
|
|
419
|
-
// Strip markdown links: [text](url) → text
|
|
420
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
421
|
-
// Strip inline formatting: **bold**, *italic*, `code`, __underline__, _italic_, ~~strike~~
|
|
422
|
-
.replace(/(\*{1,2}|_{1,2}|`|~~)(.+?)\1/g, "$2")
|
|
423
|
-
.trim();
|
|
424
|
-
}
|
|
425
|
-
return basename(filename, extname(filename));
|
|
426
|
-
};
|
|
427
|
-
/**
|
|
428
|
-
* Parse wikilinks from markdown body: [[page]] or [[page|alias]]
|
|
429
|
-
* Strips #heading fragments so link resolution works against file paths.
|
|
430
|
-
* Strips fenced code blocks and inline code first to avoid phantom links.
|
|
431
|
-
*/
|
|
432
|
-
const parseWikilinks = (body) => {
|
|
433
|
-
// Strip fenced code blocks (```...```, ````...````, etc.) and inline code (`...`) to avoid phantom links
|
|
434
|
-
const stripped = body
|
|
435
|
-
.replace(/`{3,}[\s\S]*?`{3,}/g, "")
|
|
436
|
-
.replace(/`[^`\n]+`/g, "");
|
|
437
|
-
const links = [];
|
|
438
|
-
const regex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
439
|
-
let match;
|
|
440
|
-
while ((match = regex.exec(stripped)) !== null) {
|
|
441
|
-
// Strip #heading fragment for link resolution
|
|
442
|
-
const ref = match[1].trim().replace(/#.*$/, "").trim();
|
|
443
|
-
if (ref.length > 0) {
|
|
444
|
-
links.push(ref);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return links;
|
|
448
|
-
};
|
|
449
|
-
/**
|
|
450
|
-
* Recursively find all .md files in a directory.
|
|
451
|
-
* Includes depth limit and symlink guard to prevent infinite recursion.
|
|
452
|
-
*/
|
|
453
|
-
const findMarkdownFiles = async (dir, depth = 0) => {
|
|
454
|
-
if (depth > MAX_DIRECTORY_DEPTH)
|
|
455
|
-
return [];
|
|
456
|
-
const files = [];
|
|
457
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
458
|
-
for (const entry of entries) {
|
|
459
|
-
// Skip symlinks entirely to prevent cycles
|
|
460
|
-
if (entry.isSymbolicLink())
|
|
461
|
-
continue;
|
|
462
|
-
const fullPath = join(dir, entry.name);
|
|
463
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
464
|
-
files.push(...await findMarkdownFiles(fullPath, depth + 1));
|
|
465
|
-
}
|
|
466
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
467
|
-
files.push(fullPath);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return files;
|
|
471
|
-
};
|
|
472
|
-
/**
|
|
473
|
-
* Serialize frontmatter to YAML string with proper quoting.
|
|
474
|
-
*/
|
|
475
|
-
const serializeFrontmatter = (data) => {
|
|
476
|
-
const lines = [];
|
|
477
|
-
for (const [key, value] of Object.entries(data)) {
|
|
478
|
-
// Skip undefined values (prevents serializing as literal "undefined")
|
|
479
|
-
if (value === undefined)
|
|
480
|
-
continue;
|
|
481
|
-
if (Array.isArray(value)) {
|
|
482
|
-
// Filter only undefined; preserve null items as bare `null` keyword
|
|
483
|
-
const safe = value.filter(v => v !== undefined);
|
|
484
|
-
if (safe.length === 0) {
|
|
485
|
-
lines.push(`${key}: []`);
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
lines.push(`${key}: [${safe.map(v => v === null ? "null" :
|
|
489
|
-
(typeof v === "boolean" || typeof v === "number") ? String(v) : yamlQuoteItem(String(v))).join(", ")}]`);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
else if (value === null) {
|
|
493
|
-
// Emit bare null to preserve YAML semantics on round-trip (not quoted "null")
|
|
494
|
-
lines.push(`${key}: null`);
|
|
495
|
-
}
|
|
496
|
-
else if (typeof value === "boolean" || typeof value === "number") {
|
|
497
|
-
// Emit native booleans/numbers bare (no quoting) to preserve YAML semantics on round-trip
|
|
498
|
-
lines.push(`${key}: ${String(value)}`);
|
|
499
|
-
}
|
|
500
|
-
else if (typeof value === "object") {
|
|
501
|
-
// Serialize nested objects as JSON to prevent "[object Object]" corruption
|
|
502
|
-
lines.push(`${key}: ${yamlQuote(JSON.stringify(value))}`);
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
lines.push(`${key}: ${yamlQuote(String(value))}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return lines.join("\n");
|
|
509
|
-
};
|
|
510
|
-
/**
|
|
511
|
-
* Validate that a resolved path is contained within the given root directory.
|
|
512
|
-
* Prevents path traversal attacks.
|
|
513
|
-
*/
|
|
514
|
-
const assertPathContainment = (filePath, rootDir) => {
|
|
515
|
-
const resolvedFile = resolve(filePath);
|
|
516
|
-
const resolvedRoot = resolve(rootDir);
|
|
517
|
-
return resolvedFile.startsWith(resolvedRoot + "/") || resolvedFile === resolvedRoot;
|
|
518
|
-
};
|
|
519
|
-
/**
|
|
520
|
-
* Atomically write a file by writing to a temp file then renaming.
|
|
521
|
-
* POSIX rename() is atomic on the same filesystem, preventing partial writes
|
|
522
|
-
* from corrupting the file on crash.
|
|
523
|
-
*/
|
|
524
|
-
const atomicWriteFile = async (filePath, content) => {
|
|
525
|
-
const tmpPath = `${filePath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
526
|
-
await writeFile(tmpPath, content, "utf-8");
|
|
527
|
-
try {
|
|
528
|
-
await rename(tmpPath, filePath);
|
|
529
|
-
}
|
|
530
|
-
catch (err) {
|
|
531
|
-
// Clean up orphaned temp file on rename failure
|
|
532
|
-
await unlink(tmpPath).catch(() => { });
|
|
533
|
-
throw err;
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
// =============================================================================
|
|
537
|
-
// MemoryService
|
|
538
|
-
// =============================================================================
|
|
539
|
-
export class MemoryService extends Context.Tag("MemoryService")() {
|
|
540
|
-
}
|
|
541
|
-
export const MemoryServiceLive = Layer.effect(MemoryService, Effect.gen(function* () {
|
|
542
|
-
const docRepo = yield* MemoryDocumentRepository;
|
|
543
|
-
const linkRepo = yield* MemoryLinkRepository;
|
|
544
|
-
const propRepo = yield* MemoryPropertyRepository;
|
|
545
|
-
const sourceRepo = yield* MemorySourceRepository;
|
|
546
|
-
/**
|
|
547
|
-
* Validate that a file path is safely within its root directory.
|
|
548
|
-
*/
|
|
549
|
-
const validateFilePath = (filePath, rootDir) => Effect.gen(function* () {
|
|
550
|
-
if (!assertPathContainment(filePath, rootDir)) {
|
|
551
|
-
yield* Effect.fail(new ValidationError({ reason: `Path "${filePath}" escapes root directory "${rootDir}"` }));
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
/**
|
|
555
|
-
* Index a single markdown file. Accepts optional cached content to avoid double reads.
|
|
556
|
-
*
|
|
557
|
-
* CRASH SAFETY: Uses a two-phase hash write pattern. The document is initially upserted
|
|
558
|
-
* with fileHash="" (sentinel). Links and properties are written next. Only after all
|
|
559
|
-
* three steps complete is the real hash set via updateFileHash(). If a crash occurs
|
|
560
|
-
* between steps, incremental mode sees hash="" ≠ real hash → re-indexes the file.
|
|
561
|
-
* This prevents the stale-hash bug where a partially-indexed file is permanently
|
|
562
|
-
* skipped by incremental indexing.
|
|
563
|
-
*/
|
|
564
|
-
/** Maximum file size to index (10MB). Larger files are skipped to prevent OOM. */
|
|
565
|
-
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
566
|
-
const indexFile = (filePath, rootDir, cachedContent) => Effect.gen(function* () {
|
|
567
|
-
// Path containment check: skip files that escape the root directory
|
|
568
|
-
if (!assertPathContainment(filePath, rootDir)) {
|
|
569
|
-
return false;
|
|
570
|
-
}
|
|
571
|
-
// Check file size BEFORE reading to prevent OOM on very large files
|
|
572
|
-
const fileStat = yield* Effect.tryPromise({
|
|
573
|
-
try: () => stat(filePath),
|
|
574
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
575
|
-
});
|
|
576
|
-
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
577
|
-
return false; // Skip oversized files
|
|
578
|
-
}
|
|
579
|
-
const content = cachedContent ?? (yield* Effect.tryPromise({
|
|
580
|
-
try: () => readFile(filePath, "utf-8"),
|
|
581
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
582
|
-
}));
|
|
583
|
-
// Guard: skip binary files (null byte in first 8KB is a definitive signal)
|
|
584
|
-
if (content.slice(0, 8192).includes("\0")) {
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
const relativePath = relative(rootDir, filePath);
|
|
588
|
-
const fileHash = createHash("sha256").update(content).digest("hex");
|
|
589
|
-
const docId = generateDocId(relativePath, rootDir);
|
|
590
|
-
const { body, parsed: parsedFm } = parseFrontmatter(content);
|
|
591
|
-
const title = extractTitle(body || content, basename(filePath));
|
|
592
|
-
// Coerce tag/related items to strings: block array coercion may produce booleans/numbers/null
|
|
593
|
-
const tags = parsedFm && Array.isArray(parsedFm.tags)
|
|
594
|
-
? parsedFm.tags.filter(t => t != null).map(t => String(t))
|
|
595
|
-
: [];
|
|
596
|
-
const related = parsedFm && Array.isArray(parsedFm.related)
|
|
597
|
-
? parsedFm.related.filter(t => t != null).map(t => String(t))
|
|
598
|
-
: [];
|
|
599
|
-
const now = new Date().toISOString();
|
|
600
|
-
const createdAt = (parsedFm && typeof parsedFm.created === "string") ? parsedFm.created : fileStat.mtime.toISOString();
|
|
601
|
-
// Phase 1: Upsert with empty hash sentinel (marks "indexing in progress")
|
|
602
|
-
yield* docRepo.upsertDocument({
|
|
603
|
-
id: docId,
|
|
604
|
-
filePath: relativePath,
|
|
605
|
-
rootDir,
|
|
606
|
-
title,
|
|
607
|
-
content,
|
|
608
|
-
frontmatter: parsedFm ? JSON.stringify(parsedFm) : null,
|
|
609
|
-
tags: tags.length > 0 ? JSON.stringify(tags) : null,
|
|
610
|
-
fileHash: "",
|
|
611
|
-
fileMtime: fileStat.mtime.toISOString(),
|
|
612
|
-
createdAt,
|
|
613
|
-
indexedAt: now,
|
|
614
|
-
});
|
|
615
|
-
// Phase 2: Parse and store links (use body, not full content, to avoid wikilinks inside frontmatter/code blocks)
|
|
616
|
-
yield* linkRepo.deleteBySource(docId);
|
|
617
|
-
const wikilinks = parseWikilinks(body || content);
|
|
618
|
-
const allLinks = [
|
|
619
|
-
...wikilinks.map(ref => ({ sourceDocId: docId, targetRef: ref, linkType: "wikilink" })),
|
|
620
|
-
...related.map(ref => ({ sourceDocId: docId, targetRef: ref, linkType: "frontmatter" })),
|
|
621
|
-
];
|
|
622
|
-
if (allLinks.length > 0) {
|
|
623
|
-
yield* linkRepo.insertLinks(allLinks);
|
|
624
|
-
}
|
|
625
|
-
// Sync properties from frontmatter (non-reserved keys).
|
|
626
|
-
// Always call syncFromFrontmatter even when parsedFm is null to clear stale properties.
|
|
627
|
-
// Stringify non-string primitives (booleans, numbers) so they're queryable via --prop.
|
|
628
|
-
const properties = {};
|
|
629
|
-
if (parsedFm) {
|
|
630
|
-
for (const [key, value] of Object.entries(parsedFm)) {
|
|
631
|
-
if (RESERVED_FRONTMATTER_KEYS.has(key))
|
|
632
|
-
continue;
|
|
633
|
-
if (value === null || value === undefined)
|
|
634
|
-
continue;
|
|
635
|
-
if (Array.isArray(value) || typeof value === "object")
|
|
636
|
-
continue;
|
|
637
|
-
properties[key] = String(value);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
yield* propRepo.syncFromFrontmatter(docId, properties);
|
|
641
|
-
// Phase 3: Set real hash — only after links + properties are fully written.
|
|
642
|
-
// Incremental mode checks this hash; empty sentinel ≠ real hash → re-index.
|
|
643
|
-
yield* docRepo.updateFileHash(docId, fileHash);
|
|
644
|
-
return true;
|
|
645
|
-
});
|
|
646
|
-
return {
|
|
647
|
-
addSource: (dir, label) => Effect.gen(function* () {
|
|
648
|
-
const absDir = resolve(dir);
|
|
649
|
-
// Verify directory exists before registering
|
|
650
|
-
const dirStat = yield* Effect.tryPromise({
|
|
651
|
-
try: () => stat(absDir),
|
|
652
|
-
catch: () => new ValidationError({ reason: `Directory does not exist: ${absDir}` })
|
|
653
|
-
});
|
|
654
|
-
if (!dirStat.isDirectory()) {
|
|
655
|
-
return yield* Effect.fail(new ValidationError({ reason: `Not a directory: ${absDir}` }));
|
|
656
|
-
}
|
|
657
|
-
return yield* sourceRepo.addSource(absDir, label);
|
|
658
|
-
}),
|
|
659
|
-
removeSource: (dir) => Effect.gen(function* () {
|
|
660
|
-
const absDir = resolve(dir);
|
|
661
|
-
const source = yield* sourceRepo.findSource(absDir);
|
|
662
|
-
if (!source) {
|
|
663
|
-
yield* Effect.fail(new MemorySourceNotFoundError({ rootDir: absDir }));
|
|
664
|
-
}
|
|
665
|
-
// Atomic: nulls incoming links → deletes docs → deletes source in one transaction
|
|
666
|
-
yield* sourceRepo.removeSource(absDir);
|
|
667
|
-
}),
|
|
668
|
-
listSources: () => sourceRepo.listSources(),
|
|
669
|
-
createDocument: (input) => Effect.gen(function* () {
|
|
670
|
-
// Validate title produces a non-empty slug
|
|
671
|
-
const slug = slugify(input.title);
|
|
672
|
-
if (slug.length === 0) {
|
|
673
|
-
return yield* Effect.fail(new ValidationError({
|
|
674
|
-
reason: `Title "${input.title}" produces an empty filename after slugification`
|
|
675
|
-
}));
|
|
676
|
-
}
|
|
677
|
-
// Determine target directory
|
|
678
|
-
const sources = yield* sourceRepo.listSources();
|
|
679
|
-
const targetDir = input.dir ? resolve(input.dir) : (sources[0]?.rootDir ?? resolve(".tx", "memory"));
|
|
680
|
-
// Resolve rootDir: find the registered source that contains targetDir
|
|
681
|
-
// (important when targetDir is a subdirectory of a source)
|
|
682
|
-
let matchingSource = sources.find(s => targetDir.startsWith(s.rootDir + "/") || targetDir === s.rootDir);
|
|
683
|
-
// Validate target directory is within a registered source (if dir was explicitly provided)
|
|
684
|
-
if (input.dir && !matchingSource) {
|
|
685
|
-
return yield* Effect.fail(new ValidationError({
|
|
686
|
-
reason: `Directory "${targetDir}" is not within any registered memory source`
|
|
687
|
-
}));
|
|
688
|
-
}
|
|
689
|
-
// Auto-register fallback directory as a source so documents survive future index() runs
|
|
690
|
-
// (without this, docs created in an unregistered dir become ghosts)
|
|
691
|
-
if (!matchingSource) {
|
|
692
|
-
matchingSource = yield* sourceRepo.addSource(targetDir, "auto");
|
|
693
|
-
}
|
|
694
|
-
// Use the matching source's rootDir for proper relative path calculation
|
|
695
|
-
const rootDir = matchingSource.rootDir;
|
|
696
|
-
// Ensure directory exists
|
|
697
|
-
yield* Effect.tryPromise({
|
|
698
|
-
try: () => mkdir(targetDir, { recursive: true }),
|
|
699
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
700
|
-
});
|
|
701
|
-
// Generate filename
|
|
702
|
-
const filename = `${slug}.md`;
|
|
703
|
-
const filePath = join(targetDir, filename);
|
|
704
|
-
// Check if file already exists to prevent silent overwrite
|
|
705
|
-
const fileExists = yield* Effect.tryPromise({
|
|
706
|
-
try: async () => {
|
|
707
|
-
try {
|
|
708
|
-
await stat(filePath);
|
|
709
|
-
return true;
|
|
710
|
-
}
|
|
711
|
-
catch {
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
},
|
|
715
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
716
|
-
});
|
|
717
|
-
if (fileExists) {
|
|
718
|
-
return yield* Effect.fail(new ValidationError({
|
|
719
|
-
reason: `File already exists: ${filePath}`
|
|
720
|
-
}));
|
|
721
|
-
}
|
|
722
|
-
// Build content with frontmatter
|
|
723
|
-
const fmData = {};
|
|
724
|
-
if (input.tags && input.tags.length > 0)
|
|
725
|
-
fmData.tags = [...input.tags];
|
|
726
|
-
fmData.created = new Date().toISOString();
|
|
727
|
-
if (input.properties) {
|
|
728
|
-
for (const [key, value] of Object.entries(input.properties)) {
|
|
729
|
-
if (RESERVED_FRONTMATTER_KEYS.has(key)) {
|
|
730
|
-
return yield* Effect.fail(new ValidationError({
|
|
731
|
-
reason: `Property key "${key}" is reserved; use the tags/content fields instead`
|
|
732
|
-
}));
|
|
733
|
-
}
|
|
734
|
-
fmData[key] = value;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
let fileContent = "";
|
|
738
|
-
if (Object.keys(fmData).length > 0) {
|
|
739
|
-
fileContent += `---\n${serializeFrontmatter(fmData)}\n---\n\n`;
|
|
740
|
-
}
|
|
741
|
-
fileContent += `# ${input.title}\n\n${input.content ?? ""}\n`;
|
|
742
|
-
// Write file atomically (temp + rename prevents partial writes on crash)
|
|
743
|
-
yield* Effect.tryPromise({
|
|
744
|
-
try: () => atomicWriteFile(filePath, fileContent),
|
|
745
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
746
|
-
});
|
|
747
|
-
// Index the new file using the resolved rootDir
|
|
748
|
-
yield* indexFile(filePath, rootDir, fileContent);
|
|
749
|
-
// Return the indexed document
|
|
750
|
-
const relativePath = relative(rootDir, filePath);
|
|
751
|
-
const docId = generateDocId(relativePath, rootDir);
|
|
752
|
-
const doc = yield* docRepo.findById(docId);
|
|
753
|
-
if (!doc) {
|
|
754
|
-
return yield* Effect.fail(new DatabaseError({ cause: new Error("Document not found after indexing") }));
|
|
755
|
-
}
|
|
756
|
-
return doc;
|
|
757
|
-
}),
|
|
758
|
-
updateFrontmatter: (id, updates) => Effect.gen(function* () {
|
|
759
|
-
const doc = yield* docRepo.findById(id);
|
|
760
|
-
if (!doc) {
|
|
761
|
-
return yield* Effect.fail(new MemoryDocumentNotFoundError({ id }));
|
|
762
|
-
}
|
|
763
|
-
const filePath = join(doc.rootDir, doc.filePath);
|
|
764
|
-
yield* validateFilePath(filePath, doc.rootDir);
|
|
765
|
-
const content = yield* Effect.tryPromise({
|
|
766
|
-
try: () => readFile(filePath, "utf-8"),
|
|
767
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
768
|
-
});
|
|
769
|
-
const { parsed: parsedFm, body } = parseFrontmatter(content);
|
|
770
|
-
const fm = parsedFm ?? {};
|
|
771
|
-
// Update tags: coerce to strings (block array coercion may produce booleans/numbers)
|
|
772
|
-
// and filter empty/whitespace-only tags to prevent "" entries in frontmatter
|
|
773
|
-
let tags = Array.isArray(fm.tags)
|
|
774
|
-
? fm.tags.filter(t => t != null).map(t => String(t))
|
|
775
|
-
: [];
|
|
776
|
-
if (updates.addTags) {
|
|
777
|
-
for (const tag of updates.addTags) {
|
|
778
|
-
if (tag.trim().length === 0)
|
|
779
|
-
continue;
|
|
780
|
-
if (!tags.includes(tag))
|
|
781
|
-
tags.push(tag);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
if (updates.removeTags) {
|
|
785
|
-
tags = tags.filter((t) => !updates.removeTags.includes(t));
|
|
786
|
-
}
|
|
787
|
-
fm.tags = tags;
|
|
788
|
-
// Update related
|
|
789
|
-
if (updates.addRelated) {
|
|
790
|
-
const related = Array.isArray(fm.related) ? [...fm.related] : [];
|
|
791
|
-
for (const ref of updates.addRelated) {
|
|
792
|
-
if (!related.includes(ref))
|
|
793
|
-
related.push(ref);
|
|
794
|
-
}
|
|
795
|
-
fm.related = related;
|
|
796
|
-
}
|
|
797
|
-
// Rewrite file atomically
|
|
798
|
-
const newContent = `---\n${serializeFrontmatter(fm)}\n---\n${body}`;
|
|
799
|
-
yield* Effect.tryPromise({
|
|
800
|
-
try: () => atomicWriteFile(filePath, newContent),
|
|
801
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
802
|
-
});
|
|
803
|
-
// Re-index
|
|
804
|
-
yield* indexFile(filePath, doc.rootDir, newContent);
|
|
805
|
-
const updated = yield* docRepo.findById(id);
|
|
806
|
-
if (!updated) {
|
|
807
|
-
return yield* Effect.fail(new MemoryDocumentNotFoundError({ id }));
|
|
808
|
-
}
|
|
809
|
-
return updated;
|
|
810
|
-
}),
|
|
811
|
-
setProperty: (id, key, value) => Effect.gen(function* () {
|
|
812
|
-
// Guard reserved frontmatter keys
|
|
813
|
-
if (RESERVED_FRONTMATTER_KEYS.has(key)) {
|
|
814
|
-
return yield* Effect.fail(new ValidationError({
|
|
815
|
-
reason: `Key "${key}" is reserved; use updateFrontmatter to modify tags/related/created`
|
|
816
|
-
}));
|
|
817
|
-
}
|
|
818
|
-
// Validate key format: must be a valid YAML bare key (letters, digits, dots, hyphens)
|
|
819
|
-
// Keys with colons, slashes, newlines etc. corrupt frontmatter on round-trip
|
|
820
|
-
if (!/^\w[\w.-]*$/.test(key)) {
|
|
821
|
-
return yield* Effect.fail(new ValidationError({
|
|
822
|
-
reason: `Property key "${key}" contains invalid characters. Keys must match [a-zA-Z0-9_][a-zA-Z0-9_.-]*`
|
|
823
|
-
}));
|
|
824
|
-
}
|
|
825
|
-
const doc = yield* docRepo.findById(id);
|
|
826
|
-
if (!doc) {
|
|
827
|
-
return yield* Effect.fail(new MemoryDocumentNotFoundError({ id }));
|
|
828
|
-
}
|
|
829
|
-
// Write file first (filesystem is source of truth — if this fails, DB stays consistent)
|
|
830
|
-
const filePath = join(doc.rootDir, doc.filePath);
|
|
831
|
-
yield* validateFilePath(filePath, doc.rootDir);
|
|
832
|
-
const content = yield* Effect.tryPromise({
|
|
833
|
-
try: () => readFile(filePath, "utf-8"),
|
|
834
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
835
|
-
});
|
|
836
|
-
const { parsed: parsedFm, body } = parseFrontmatter(content);
|
|
837
|
-
const hadFrontmatter = parsedFm !== null;
|
|
838
|
-
const fm = parsedFm ?? {};
|
|
839
|
-
fm[key] = value;
|
|
840
|
-
// When adding frontmatter to a file that had none, ensure blank line separator
|
|
841
|
-
const separator = hadFrontmatter ? "" : "\n";
|
|
842
|
-
const newContent = `---\n${serializeFrontmatter(fm)}\n---\n${separator}${body}`;
|
|
843
|
-
yield* Effect.tryPromise({
|
|
844
|
-
try: () => atomicWriteFile(filePath, newContent),
|
|
845
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
846
|
-
});
|
|
847
|
-
// Re-index to keep DB hash/frontmatter/content in sync (same pattern as updateFrontmatter)
|
|
848
|
-
yield* indexFile(filePath, doc.rootDir, newContent);
|
|
849
|
-
}),
|
|
850
|
-
getProperties: (id) => propRepo.getProperties(id),
|
|
851
|
-
removeProperty: (id, key) => Effect.gen(function* () {
|
|
852
|
-
// Guard reserved frontmatter keys
|
|
853
|
-
if (RESERVED_FRONTMATTER_KEYS.has(key)) {
|
|
854
|
-
return yield* Effect.fail(new ValidationError({
|
|
855
|
-
reason: `Key "${key}" is reserved; use updateFrontmatter to modify tags/related/created`
|
|
856
|
-
}));
|
|
857
|
-
}
|
|
858
|
-
const doc = yield* docRepo.findById(id);
|
|
859
|
-
if (!doc) {
|
|
860
|
-
return yield* Effect.fail(new MemoryDocumentNotFoundError({ id }));
|
|
861
|
-
}
|
|
862
|
-
// Write file first (filesystem is source of truth — if this fails, DB stays consistent)
|
|
863
|
-
const filePath = join(doc.rootDir, doc.filePath);
|
|
864
|
-
yield* validateFilePath(filePath, doc.rootDir);
|
|
865
|
-
const content = yield* Effect.tryPromise({
|
|
866
|
-
try: () => readFile(filePath, "utf-8"),
|
|
867
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
868
|
-
});
|
|
869
|
-
const { parsed: parsedFm, body } = parseFrontmatter(content);
|
|
870
|
-
if (parsedFm && key in parsedFm) {
|
|
871
|
-
delete parsedFm[key];
|
|
872
|
-
// Only write frontmatter block if there are remaining keys; otherwise write body only
|
|
873
|
-
const newContent = Object.keys(parsedFm).length > 0
|
|
874
|
-
? `---\n${serializeFrontmatter(parsedFm)}\n---\n${body}`
|
|
875
|
-
: body;
|
|
876
|
-
yield* Effect.tryPromise({
|
|
877
|
-
try: () => atomicWriteFile(filePath, newContent),
|
|
878
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
879
|
-
});
|
|
880
|
-
// Re-index to keep DB hash/frontmatter/content in sync
|
|
881
|
-
yield* indexFile(filePath, doc.rootDir, newContent);
|
|
882
|
-
}
|
|
883
|
-
else {
|
|
884
|
-
// Key not in frontmatter — just delete from DB if it exists
|
|
885
|
-
yield* propRepo.deleteProperty(id, key);
|
|
886
|
-
}
|
|
887
|
-
}),
|
|
888
|
-
index: (options) => Effect.gen(function* () {
|
|
889
|
-
const sources = yield* sourceRepo.listSources();
|
|
890
|
-
let indexed = 0;
|
|
891
|
-
let skipped = 0;
|
|
892
|
-
let removed = 0;
|
|
893
|
-
for (const source of sources) {
|
|
894
|
-
const existingPaths = new Set(yield* docRepo.listPathsByRootDir(source.rootDir));
|
|
895
|
-
// Find all .md files (gracefully handle deleted source directories)
|
|
896
|
-
const files = yield* Effect.tryPromise({
|
|
897
|
-
try: () => findMarkdownFiles(source.rootDir),
|
|
898
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
899
|
-
}).pipe(Effect.catchAll(() => Effect.succeed([])));
|
|
900
|
-
for (const filePath of files) {
|
|
901
|
-
const relativePath = relative(source.rootDir, filePath);
|
|
902
|
-
// Wrap each file in catchAll so a single unreadable file
|
|
903
|
-
// (permission denied, encoding error, etc.) doesn't abort the entire run.
|
|
904
|
-
const fileResult = yield* Effect.gen(function* () {
|
|
905
|
-
if (options?.incremental) {
|
|
906
|
-
// Check file size BEFORE reading to prevent OOM on large files
|
|
907
|
-
const fileStat = yield* Effect.tryPromise({
|
|
908
|
-
try: () => stat(filePath),
|
|
909
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
910
|
-
});
|
|
911
|
-
if (fileStat.size > 10 * 1024 * 1024) {
|
|
912
|
-
return "skipped"; // Skip oversized files (>10MB)
|
|
913
|
-
}
|
|
914
|
-
// Read file once for hash check; pass cached content to indexFile to avoid double read
|
|
915
|
-
const content = yield* Effect.tryPromise({
|
|
916
|
-
try: () => readFile(filePath, "utf-8"),
|
|
917
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
918
|
-
});
|
|
919
|
-
const fileHash = createHash("sha256").update(content).digest("hex");
|
|
920
|
-
const existing = yield* docRepo.findByPath(relativePath, source.rootDir);
|
|
921
|
-
if (existing && existing.fileHash === fileHash) {
|
|
922
|
-
return "skipped";
|
|
923
|
-
}
|
|
924
|
-
// File changed — index with cached content (no double read)
|
|
925
|
-
yield* indexFile(filePath, source.rootDir, content);
|
|
926
|
-
}
|
|
927
|
-
else {
|
|
928
|
-
yield* indexFile(filePath, source.rootDir);
|
|
929
|
-
}
|
|
930
|
-
return "indexed";
|
|
931
|
-
}).pipe(Effect.catchAll(() => Effect.succeed("error")));
|
|
932
|
-
// Only remove from "needs cleanup" set on success — if file read failed
|
|
933
|
-
// (TOCTOU: deleted between listing and reading), keep the path in the set
|
|
934
|
-
// so its stale DB entry is cleaned up in the deletion pass below.
|
|
935
|
-
if (fileResult !== "error") {
|
|
936
|
-
existingPaths.delete(relativePath);
|
|
937
|
-
}
|
|
938
|
-
if (fileResult === "indexed")
|
|
939
|
-
indexed++;
|
|
940
|
-
else if (fileResult === "skipped")
|
|
941
|
-
skipped++;
|
|
942
|
-
// "error" — silently skip; stale DB entry will be cleaned up below
|
|
943
|
-
}
|
|
944
|
-
// Remove docs for deleted files
|
|
945
|
-
const deletedPaths = [...existingPaths];
|
|
946
|
-
if (deletedPaths.length > 0) {
|
|
947
|
-
const count = yield* docRepo.deleteByPaths(source.rootDir, deletedPaths);
|
|
948
|
-
removed += count;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
// Resolve link targets
|
|
952
|
-
yield* linkRepo.resolveTargets();
|
|
953
|
-
return { indexed, skipped, removed };
|
|
954
|
-
}),
|
|
955
|
-
indexStatus: () => Effect.gen(function* () {
|
|
956
|
-
const sources = yield* sourceRepo.listSources();
|
|
957
|
-
const totalDocs = yield* docRepo.count();
|
|
958
|
-
const embedded = yield* docRepo.countWithEmbeddings();
|
|
959
|
-
const links = yield* linkRepo.count();
|
|
960
|
-
// Count total .md files across all sources (skip sources whose dirs were deleted)
|
|
961
|
-
// Also count files not yet in DB (new) and DB entries with no file on disk (orphaned)
|
|
962
|
-
let totalFiles = 0;
|
|
963
|
-
let notIndexed = 0;
|
|
964
|
-
for (const source of sources) {
|
|
965
|
-
const files = yield* Effect.tryPromise({
|
|
966
|
-
try: () => findMarkdownFiles(source.rootDir),
|
|
967
|
-
catch: (cause) => new DatabaseError({ cause })
|
|
968
|
-
}).pipe(Effect.catchAll(() => Effect.succeed([])));
|
|
969
|
-
totalFiles += files.length;
|
|
970
|
-
// Count files not yet in DB
|
|
971
|
-
const indexedPaths = new Set(yield* docRepo.listPathsByRootDir(source.rootDir));
|
|
972
|
-
for (const filePath of files) {
|
|
973
|
-
const rel = relative(source.rootDir, filePath);
|
|
974
|
-
if (!indexedPaths.has(rel))
|
|
975
|
-
notIndexed++;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return {
|
|
979
|
-
totalFiles,
|
|
980
|
-
indexed: totalDocs,
|
|
981
|
-
stale: notIndexed,
|
|
982
|
-
embedded,
|
|
983
|
-
links,
|
|
984
|
-
sources: sources.length,
|
|
985
|
-
};
|
|
986
|
-
}),
|
|
987
|
-
search: (query, options) => Effect.gen(function* () {
|
|
988
|
-
const limit = Math.max(1, options?.limit ?? 10);
|
|
989
|
-
const minScore = options?.minScore ?? 0;
|
|
990
|
-
// Fetch extra rows when minScore filtering is active to avoid undercounting
|
|
991
|
-
const fetchLimit = minScore > 0 ? limit * 3 : limit;
|
|
992
|
-
// BM25 search
|
|
993
|
-
let bm25Results = yield* docRepo.searchBM25(query, fetchLimit);
|
|
994
|
-
// Filter by tags if specified (case-insensitive)
|
|
995
|
-
if (options?.tags && options.tags.length > 0) {
|
|
996
|
-
const tagFilterLower = options.tags.map((t) => t.toLowerCase());
|
|
997
|
-
bm25Results = bm25Results.filter(r => tagFilterLower.every((t) => r.document.tags.some((rt) => rt.toLowerCase() === t)));
|
|
998
|
-
}
|
|
999
|
-
// Filter by properties if specified
|
|
1000
|
-
if (options?.props && options.props.length > 0) {
|
|
1001
|
-
for (const propFilter of options.props) {
|
|
1002
|
-
const eqIdx = propFilter.indexOf("=");
|
|
1003
|
-
const key = eqIdx >= 0 ? propFilter.slice(0, eqIdx) : propFilter;
|
|
1004
|
-
const value = eqIdx >= 0 ? propFilter.slice(eqIdx + 1) : undefined;
|
|
1005
|
-
if (!key || key.trim().length === 0)
|
|
1006
|
-
continue;
|
|
1007
|
-
const matchingDocIds = new Set(yield* propRepo.findByProperty(key.trim(), value));
|
|
1008
|
-
bm25Results = bm25Results.filter(r => matchingDocIds.has(r.document.id));
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
// Recency scoring: 30-day decay (same weight as retriever for consistent scores)
|
|
1012
|
-
const RECENCY_WEIGHT = 0.1;
|
|
1013
|
-
const now = Date.now();
|
|
1014
|
-
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1015
|
-
// Convert to scored results with recency blend matching retriever formula
|
|
1016
|
-
const results = bm25Results
|
|
1017
|
-
.map((r, rank) => {
|
|
1018
|
-
const mtimeMs = new Date(r.document.fileMtime).getTime();
|
|
1019
|
-
// Clamp future-dated files to now (prevents negative age / score > 1)
|
|
1020
|
-
const safeMs = isNaN(mtimeMs) ? now : mtimeMs;
|
|
1021
|
-
const ageMs = Math.max(0, now - Math.min(safeMs, now));
|
|
1022
|
-
const recencyScore = Math.max(0, 1.0 - (ageMs / THIRTY_DAYS_MS));
|
|
1023
|
-
// Blend BM25 + recency using same formula as retriever for consistent scores
|
|
1024
|
-
const relevanceScore = (1 - RECENCY_WEIGHT) * r.score + RECENCY_WEIGHT * recencyScore;
|
|
1025
|
-
return {
|
|
1026
|
-
...r.document,
|
|
1027
|
-
relevanceScore,
|
|
1028
|
-
bm25Score: r.score,
|
|
1029
|
-
vectorScore: 0,
|
|
1030
|
-
rrfScore: 0, // No RRF fusion in BM25-only path; 0 indicates single-list mode
|
|
1031
|
-
recencyScore,
|
|
1032
|
-
bm25Rank: rank + 1,
|
|
1033
|
-
vectorRank: 0,
|
|
1034
|
-
};
|
|
1035
|
-
})
|
|
1036
|
-
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
1037
|
-
.filter(r => r.relevanceScore >= minScore)
|
|
1038
|
-
.slice(0, limit);
|
|
1039
|
-
return results;
|
|
1040
|
-
}),
|
|
1041
|
-
getDocument: (id) => Effect.gen(function* () {
|
|
1042
|
-
const doc = yield* docRepo.findById(id);
|
|
1043
|
-
if (!doc) {
|
|
1044
|
-
return yield* Effect.fail(new MemoryDocumentNotFoundError({ id }));
|
|
1045
|
-
}
|
|
1046
|
-
return doc;
|
|
1047
|
-
}),
|
|
1048
|
-
getLinks: (id) => linkRepo.findOutgoing(id),
|
|
1049
|
-
getBacklinks: (id) => linkRepo.findIncoming(id),
|
|
1050
|
-
addLink: (sourceId, targetRef) => Effect.gen(function* () {
|
|
1051
|
-
// Validate source document exists to prevent phantom links
|
|
1052
|
-
const doc = yield* docRepo.findById(sourceId);
|
|
1053
|
-
if (!doc) {
|
|
1054
|
-
return yield* Effect.fail(new MemoryDocumentNotFoundError({ id: sourceId }));
|
|
1055
|
-
}
|
|
1056
|
-
yield* linkRepo.insertExplicit(sourceId, targetRef);
|
|
1057
|
-
}),
|
|
1058
|
-
listDocuments: (filter) => docRepo.listAll(filter ? { rootDir: filter.source, tags: filter.tags ? [...filter.tags] : undefined } : undefined),
|
|
1059
|
-
};
|
|
1060
|
-
}));
|
|
1
|
+
export { MemoryService, MemoryServiceLive, } from "../internal/memory-service-impl.js";
|
|
1061
2
|
//# sourceMappingURL=memory-service.js.map
|