devchain-cli 0.22.1 → 0.23.1
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/drizzle/0077_familiar_morbius.sql +6 -0
- package/dist/drizzle/0078_preserve_legacy_connection_ownership.sql +180 -0
- package/dist/drizzle/0079_premium_forgotten_one.sql +18 -0
- package/dist/drizzle/0080_lethal_thunderbolt_ross.sql +49 -0
- package/dist/drizzle/0081_early_kronos.sql +58 -0
- package/dist/drizzle/0082_huge_black_queen.sql +14 -0
- package/dist/drizzle/meta/0077_snapshot.json +6957 -0
- package/dist/drizzle/meta/0078_snapshot.json +6957 -0
- package/dist/drizzle/meta/0079_snapshot.json +7093 -0
- package/dist/drizzle/meta/0080_snapshot.json +7241 -0
- package/dist/drizzle/meta/0081_snapshot.json +7482 -0
- package/dist/drizzle/meta/0082_snapshot.json +7571 -0
- package/dist/drizzle/meta/_journal.json +42 -0
- package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
- package/dist/server/common/errors/error-types.d.ts +7 -0
- package/dist/server/common/errors/error-types.js +7 -1
- package/dist/server/common/errors/error-types.js.map +1 -1
- package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.d.ts +9 -0
- package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.js +90 -0
- package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.js.map +1 -0
- package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.d.ts +12 -0
- package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.js +322 -0
- package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.js.map +1 -0
- package/dist/server/modules/epic-time/epic-time-store.module.d.ts +2 -0
- package/dist/server/modules/epic-time/epic-time-store.module.js +23 -0
- package/dist/server/modules/epic-time/epic-time-store.module.js.map +1 -0
- package/dist/server/modules/epic-time/epic-time.module.js +10 -6
- package/dist/server/modules/epic-time/epic-time.module.js.map +1 -1
- package/dist/server/modules/epic-time/models/epic-time-daily-allocator.d.ts +30 -0
- package/dist/server/modules/epic-time/models/epic-time-daily-allocator.js +125 -0
- package/dist/server/modules/epic-time/models/epic-time-daily-allocator.js.map +1 -0
- package/dist/server/modules/epic-time/models/epic-time-local-day.d.ts +9 -0
- package/dist/server/modules/epic-time/models/epic-time-local-day.js +164 -0
- package/dist/server/modules/epic-time/models/epic-time-local-day.js.map +1 -0
- package/dist/server/modules/epic-time/models/epic-time.models.d.ts +147 -0
- package/dist/server/modules/epic-time/models/epic-time.models.js +2 -0
- package/dist/server/modules/epic-time/models/epic-time.models.js.map +1 -1
- package/dist/server/modules/epic-time/services/agent-time-accounting.service.d.ts +1 -0
- package/dist/server/modules/epic-time/services/agent-time-accounting.service.js +31 -8
- package/dist/server/modules/epic-time/services/agent-time-accounting.service.js.map +1 -1
- package/dist/server/modules/epic-time/services/epic-estimate-logging.service.d.ts +37 -0
- package/dist/server/modules/epic-time/services/epic-estimate-logging.service.js +716 -0
- package/dist/server/modules/epic-time/services/epic-estimate-logging.service.js.map +1 -0
- package/dist/server/modules/epic-time/services/epic-time.service.d.ts +13 -3
- package/dist/server/modules/epic-time/services/epic-time.service.js +162 -69
- package/dist/server/modules/epic-time/services/epic-time.service.js.map +1 -1
- package/dist/server/modules/epic-time/services/epic-time.store.d.ts +50 -3
- package/dist/server/modules/epic-time/services/epic-time.store.js +705 -146
- package/dist/server/modules/epic-time/services/epic-time.store.js.map +1 -1
- package/dist/server/modules/epics/controllers/epics.controller.d.ts +10 -1
- package/dist/server/modules/epics/controllers/epics.controller.js +108 -2
- package/dist/server/modules/epics/controllers/epics.controller.js.map +1 -1
- package/dist/server/modules/epics/epics.module.js +8 -2
- package/dist/server/modules/epics/epics.module.js.map +1 -1
- package/dist/server/modules/epics/models/epic-relations.models.d.ts +34 -0
- package/dist/server/modules/epics/models/epic-relations.models.js +16 -0
- package/dist/server/modules/epics/models/epic-relations.models.js.map +1 -0
- package/dist/server/modules/epics/services/epic-relation-target-resolver.d.ts +8 -0
- package/dist/server/modules/epics/services/epic-relation-target-resolver.js +37 -0
- package/dist/server/modules/epics/services/epic-relation-target-resolver.js.map +1 -0
- package/dist/server/modules/epics/services/epic-relations.service.d.ts +52 -0
- package/dist/server/modules/epics/services/epic-relations.service.js +135 -0
- package/dist/server/modules/epics/services/epic-relations.service.js.map +1 -0
- package/dist/server/modules/epics/services/epics.service.d.ts +9 -2
- package/dist/server/modules/epics/services/epics.service.js +115 -11
- package/dist/server/modules/epics/services/epics.service.js.map +1 -1
- package/dist/server/modules/events/catalog/broadcast-registry.js +16 -0
- package/dist/server/modules/events/catalog/broadcast-registry.js.map +1 -1
- package/dist/server/modules/events/catalog/epic.relations.invalidated.d.ts +12 -0
- package/dist/server/modules/events/catalog/epic.relations.invalidated.js +13 -0
- package/dist/server/modules/events/catalog/epic.relations.invalidated.js.map +1 -0
- package/dist/server/modules/events/catalog/epic.time.scope.invalidated.d.ts +12 -0
- package/dist/server/modules/events/catalog/epic.time.scope.invalidated.js +13 -0
- package/dist/server/modules/events/catalog/epic.time.scope.invalidated.js.map +1 -0
- package/dist/server/modules/events/catalog/index.d.ts +27 -4
- package/dist/server/modules/events/catalog/index.js +6 -0
- package/dist/server/modules/events/catalog/index.js.map +1 -1
- package/dist/server/modules/events/catalog/integration.connection.created.d.ts +4 -1
- package/dist/server/modules/events/catalog/integration.connection.created.js +5 -2
- package/dist/server/modules/events/catalog/integration.connection.created.js.map +1 -1
- package/dist/server/modules/events/catalog/integration.connection.deleted.d.ts +4 -1
- package/dist/server/modules/events/catalog/integration.connection.deleted.js +5 -2
- package/dist/server/modules/events/catalog/integration.connection.deleted.js.map +1 -1
- package/dist/server/modules/events/catalog/integration.connection.legacy.d.ts +85 -0
- package/dist/server/modules/events/catalog/integration.connection.legacy.js +41 -0
- package/dist/server/modules/events/catalog/integration.connection.legacy.js.map +1 -0
- package/dist/server/modules/events/catalog/integration.connection.updated.d.ts +4 -1
- package/dist/server/modules/events/catalog/integration.connection.updated.js +5 -2
- package/dist/server/modules/events/catalog/integration.connection.updated.js.map +1 -1
- package/dist/server/modules/events/services/committed-event-replay.d.ts +10 -0
- package/dist/server/modules/events/services/committed-event-replay.js +30 -0
- package/dist/server/modules/events/services/committed-event-replay.js.map +1 -0
- package/dist/server/modules/events/services/committed-event.store.d.ts +1 -0
- package/dist/server/modules/events/services/committed-event.store.js +12 -7
- package/dist/server/modules/events/services/committed-event.store.js.map +1 -1
- package/dist/server/modules/events/services/durable-event-registry.service.d.ts +9 -2
- package/dist/server/modules/events/services/durable-event-registry.service.js.map +1 -1
- package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.d.ts +7 -0
- package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.js +158 -50
- package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.js.map +1 -1
- package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.d.ts +4 -0
- package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.js +73 -33
- package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.js.map +1 -1
- package/dist/server/modules/external-integrations/connections/integration-connections.controller.d.ts +6 -3
- package/dist/server/modules/external-integrations/connections/integration-connections.controller.js +67 -12
- package/dist/server/modules/external-integrations/connections/integration-connections.controller.js.map +1 -1
- package/dist/server/modules/external-integrations/connections/integration-connections.service.d.ts +35 -3
- package/dist/server/modules/external-integrations/connections/integration-connections.service.js +178 -17
- package/dist/server/modules/external-integrations/connections/integration-connections.service.js.map +1 -1
- package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.d.ts +6 -3
- package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.js +51 -11
- package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.js.map +1 -1
- package/dist/server/modules/external-integrations/external-integrations.module.js +6 -1
- package/dist/server/modules/external-integrations/external-integrations.module.js.map +1 -1
- package/dist/server/modules/external-integrations/models/external-edit-session.models.d.ts +1 -0
- package/dist/server/modules/external-integrations/models/external-provider.models.d.ts +6 -2
- package/dist/server/modules/external-integrations/models/external-provider.models.js +2 -3
- package/dist/server/modules/external-integrations/models/external-provider.models.js.map +1 -1
- package/dist/server/modules/external-integrations/models/external-time-mutation.models.d.ts +33 -2
- package/dist/server/modules/external-integrations/models/external-time-mutation.models.js +14 -0
- package/dist/server/modules/external-integrations/models/external-time-mutation.models.js.map +1 -1
- package/dist/server/modules/external-integrations/my-work/external-edit-session.service.d.ts +10 -9
- package/dist/server/modules/external-integrations/my-work/external-edit-session.service.js +81 -46
- package/dist/server/modules/external-integrations/my-work/external-edit-session.service.js.map +1 -1
- package/dist/server/modules/external-integrations/my-work/external-my-work.controller.d.ts +22 -20
- package/dist/server/modules/external-integrations/my-work/external-my-work.controller.js +136 -64
- package/dist/server/modules/external-integrations/my-work/external-my-work.controller.js.map +1 -1
- package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.d.ts +7 -0
- package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.js +289 -4
- package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.js.map +1 -1
- package/dist/server/modules/external-integrations/my-work/external-my-work.service.d.ts +12 -7
- package/dist/server/modules/external-integrations/my-work/external-my-work.service.js +114 -54
- package/dist/server/modules/external-integrations/my-work/external-my-work.service.js.map +1 -1
- package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.d.ts +19 -6
- package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.js +346 -46
- package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.js.map +1 -1
- package/dist/server/modules/external-integrations/sessions/external-edit-session.store.d.ts +2 -1
- package/dist/server/modules/external-integrations/sessions/external-edit-session.store.js +3 -2
- package/dist/server/modules/external-integrations/sessions/external-edit-session.store.js.map +1 -1
- package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.d.ts +6 -1
- package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.js +19 -0
- package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.js.map +1 -1
- package/dist/server/modules/external-integrations/sessions/provider-operation-gate.d.ts +15 -2
- package/dist/server/modules/external-integrations/sessions/provider-operation-gate.js +33 -6
- package/dist/server/modules/external-integrations/sessions/provider-operation-gate.js.map +1 -1
- package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.d.ts +13 -6
- package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.js +212 -75
- package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.js.map +1 -1
- package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.d.ts +13 -4
- package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.js +98 -0
- package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.js.map +1 -1
- package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.d.ts +10 -4
- package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.js +57 -18
- package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.js.map +1 -1
- package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +139 -0
- package/dist/server/modules/mcp/dtos/mcp.dto.js +41 -1
- package/dist/server/modules/mcp/dtos/mcp.dto.js.map +1 -1
- package/dist/server/modules/mcp/services/handlers/epic-context.d.ts +2 -0
- package/dist/server/modules/mcp/services/handlers/epic-tools.d.ts +4 -0
- package/dist/server/modules/mcp/services/handlers/epic-tools.js +275 -0
- package/dist/server/modules/mcp/services/handlers/epic-tools.js.map +1 -1
- package/dist/server/modules/mcp/services/mcp-tool-binding.registry.d.ts +2 -1
- package/dist/server/modules/mcp/services/mcp-tool-binding.registry.js +13 -8
- package/dist/server/modules/mcp/services/mcp-tool-binding.registry.js.map +1 -1
- package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.d.ts +1 -1
- package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.js +1 -1
- package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.js.map +1 -1
- package/dist/server/modules/mcp/tool-descriptors/binding-types.d.ts +2 -0
- package/dist/server/modules/mcp/tool-descriptors/binding-types.js.map +1 -1
- package/dist/server/modules/mcp/tool-descriptors/epic.bindings.js +6 -0
- package/dist/server/modules/mcp/tool-descriptors/epic.bindings.js.map +1 -1
- package/dist/server/modules/mcp/tool-descriptors/epic.metadata.js +132 -0
- package/dist/server/modules/mcp/tool-descriptors/epic.metadata.js.map +1 -1
- package/dist/server/modules/projects/controllers/projects.controller.js +1 -4
- package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
- package/dist/server/modules/projects/projects.module.js +2 -0
- package/dist/server/modules/projects/projects.module.js.map +1 -1
- package/dist/server/modules/projects/services/projects.service.d.ts +4 -1
- package/dist/server/modules/projects/services/projects.service.js +23 -2
- package/dist/server/modules/projects/services/projects.service.js.map +1 -1
- package/dist/server/modules/providers/adapters/claude.adapter.d.ts +4 -2
- package/dist/server/modules/providers/adapters/claude.adapter.js +31 -0
- package/dist/server/modules/providers/adapters/claude.adapter.js.map +1 -1
- package/dist/server/modules/providers/services/provider-mcp-ensure.service.d.ts +7 -0
- package/dist/server/modules/providers/services/provider-mcp-ensure.service.js +69 -4
- package/dist/server/modules/providers/services/provider-mcp-ensure.service.js.map +1 -1
- package/dist/server/modules/session-reader/data/pricing.json +452 -56
- package/dist/server/modules/session-reader/services/session-reader.service.d.ts +2 -0
- package/dist/server/modules/session-reader/services/session-reader.service.js +85 -9
- package/dist/server/modules/session-reader/services/session-reader.service.js.map +1 -1
- package/dist/server/modules/session-reader/services/transcript-path-validator.service.js +8 -2
- package/dist/server/modules/session-reader/services/transcript-path-validator.service.js.map +1 -1
- package/dist/server/modules/session-reader/services/transcript-watcher.service.js +44 -17
- package/dist/server/modules/session-reader/services/transcript-watcher.service.js.map +1 -1
- package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.d.ts +1 -0
- package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js +1 -0
- package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js.map +1 -1
- package/dist/server/modules/sessions/services/session-runtime/session-launch-pipeline.service.js +11 -1
- package/dist/server/modules/sessions/services/session-runtime/session-launch-pipeline.service.js.map +1 -1
- package/dist/server/modules/sessions/services/sessions.service.d.ts +3 -1
- package/dist/server/modules/sessions/services/sessions.service.js +36 -6
- package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
- package/dist/server/modules/sessions/sessions.module.js +2 -0
- package/dist/server/modules/sessions/sessions.module.js.map +1 -1
- package/dist/server/modules/sessions/utils/claude-config.d.ts +4 -1
- package/dist/server/modules/sessions/utils/claude-config.js +265 -60
- package/dist/server/modules/sessions/utils/claude-config.js.map +1 -1
- package/dist/server/modules/storage/db/schema.d.ts +1109 -98
- package/dist/server/modules/storage/db/schema.js +141 -3
- package/dist/server/modules/storage/db/schema.js.map +1 -1
- package/dist/server/modules/storage/db/sqlite-json.d.ts +1 -0
- package/dist/server/modules/storage/db/sqlite-json.js +4 -0
- package/dist/server/modules/storage/db/sqlite-json.js.map +1 -1
- package/dist/server/modules/storage/interfaces/storage.interface.d.ts +58 -10
- package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
- package/dist/server/modules/storage/local/delegates/epic.delegate.d.ts +30 -5
- package/dist/server/modules/storage/local/delegates/epic.delegate.js +718 -14
- package/dist/server/modules/storage/local/delegates/epic.delegate.js.map +1 -1
- package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.d.ts +52 -0
- package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.js +659 -0
- package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.js.map +1 -0
- package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.d.ts +2 -1
- package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.js +17 -4
- package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.js.map +1 -1
- package/dist/server/modules/storage/local/delegates/integration.delegate.d.ts +23 -8
- package/dist/server/modules/storage/local/delegates/integration.delegate.js +238 -52
- package/dist/server/modules/storage/local/delegates/integration.delegate.js.map +1 -1
- package/dist/server/modules/storage/local/delegates/project.delegate.d.ts +2 -0
- package/dist/server/modules/storage/local/delegates/project.delegate.js +180 -130
- package/dist/server/modules/storage/local/delegates/project.delegate.js.map +1 -1
- package/dist/server/modules/storage/local/local-storage.service.d.ts +45 -10
- package/dist/server/modules/storage/local/local-storage.service.js +99 -15
- package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
- package/dist/server/modules/storage/models/domain.models.d.ts +170 -0
- package/dist/server/modules/storage/models/domain.models.js +9 -0
- package/dist/server/modules/storage/models/domain.models.js.map +1 -1
- package/dist/server/templates/teams-dev.json +311 -301
- package/dist/server/tsconfig.tsbuildinfo +1 -1
- package/dist/server/ui/assets/{ExternalRichEditor-DMtnZIMZ.js → ExternalRichEditor-FwCAalXO.js} +1 -1
- package/dist/server/ui/assets/{ReviewDetailPage-BmyiM5IR.js → ReviewDetailPage-D8DVpsJm.js} +1 -1
- package/dist/server/ui/assets/{ReviewsPage-CQ87BMwT.js → ReviewsPage-DcWpRFbm.js} +1 -1
- package/dist/server/ui/assets/index-CV8ydm7w.css +32 -0
- package/dist/server/ui/assets/index-Cwe7LxcW.js +1197 -0
- package/dist/server/ui/assets/{useReviewSubscription-DjbEUzt4.js → useReviewSubscription-CYoSg-88.js} +1 -1
- package/dist/server/ui/index.html +2 -2
- package/dist/templates/teams-dev.json +311 -301
- package/package.json +20 -2
- package/dist/server/ui/assets/index-BX6Yze-A.js +0 -1177
- package/dist/server/ui/assets/index-qoQEv_u4.css +0 -32
|
@@ -36,12 +36,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.EpicStorageDelegate = void 0;
|
|
37
37
|
const drizzle_orm_1 = require("drizzle-orm");
|
|
38
38
|
const node_crypto_1 = require("node:crypto");
|
|
39
|
+
const domain_models_1 = require("../../models/domain.models");
|
|
39
40
|
const error_types_1 = require("../../../../common/errors/error-types");
|
|
40
41
|
const logger_1 = require("../../../../common/logging/logger");
|
|
41
42
|
const schema_1 = require("../../db/schema");
|
|
42
43
|
const storage_helpers_1 = require("../helpers/storage-helpers");
|
|
43
44
|
const base_storage_delegate_1 = require("./base-storage.delegate");
|
|
44
45
|
const logger = (0, logger_1.createLogger)('EpicStorageDelegate');
|
|
46
|
+
const MAX_RELATION_BATCH_SIZE = 1000;
|
|
47
|
+
const MAX_RELATION_LIST_LIMIT = 100;
|
|
48
|
+
const MAX_RELATION_PREFIX_RESULTS = 100;
|
|
45
49
|
class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
46
50
|
dependencies;
|
|
47
51
|
constructor(context, dependencies) {
|
|
@@ -51,11 +55,14 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
51
55
|
async createEpic(data, eventFactory) {
|
|
52
56
|
return this.txRunner.runImmediateQueued(() => this.insertEpic(data, eventFactory));
|
|
53
57
|
}
|
|
54
|
-
async createEpicInCurrentTransaction(data, eventFactory) {
|
|
58
|
+
async createEpicInCurrentTransaction(data, eventFactory, beforeEventAppend) {
|
|
55
59
|
if (!this.rawClient.inTransaction) {
|
|
56
60
|
throw new error_types_1.StorageError('Epic transaction insert requires an active storage transaction.');
|
|
57
61
|
}
|
|
58
|
-
|
|
62
|
+
const epic = this.insertEpic(data);
|
|
63
|
+
await beforeEventAppend?.(epic);
|
|
64
|
+
this.appendEpicEvent(epic, eventFactory);
|
|
65
|
+
return epic;
|
|
59
66
|
}
|
|
60
67
|
insertEpic(data, eventFactory) {
|
|
61
68
|
const now = new Date().toISOString();
|
|
@@ -121,11 +128,14 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
121
128
|
.run();
|
|
122
129
|
}
|
|
123
130
|
}
|
|
131
|
+
this.appendEpicEvent(epic, eventFactory);
|
|
132
|
+
return epic;
|
|
133
|
+
}
|
|
134
|
+
appendEpicEvent(epic, eventFactory) {
|
|
124
135
|
const event = eventFactory?.(epic);
|
|
125
136
|
if (event) {
|
|
126
137
|
this.dependencies.appendEvent(event);
|
|
127
138
|
}
|
|
128
|
-
return epic;
|
|
129
139
|
}
|
|
130
140
|
async getEpic(id) {
|
|
131
141
|
return this.getEpicSync(id);
|
|
@@ -201,8 +211,9 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
201
211
|
};
|
|
202
212
|
}
|
|
203
213
|
async listProjectEpics(projectId, options = {}) {
|
|
204
|
-
const { epics, statuses } = await Promise.resolve().then(() => __importStar(require('../../db/schema')));
|
|
214
|
+
const { epics, statuses, externalTaskLinks } = await Promise.resolve().then(() => __importStar(require('../../db/schema')));
|
|
205
215
|
const { eq, and, sql, desc } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
|
|
216
|
+
const { safeJsonText } = await Promise.resolve().then(() => __importStar(require('../../db/sqlite-json')));
|
|
206
217
|
const limit = options.limit ?? 100;
|
|
207
218
|
const offset = options.offset ?? 0;
|
|
208
219
|
const conditions = [eq(epics.projectId, projectId)];
|
|
@@ -210,13 +221,19 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
210
221
|
const search = options.q.trim().toLowerCase();
|
|
211
222
|
if (search.length) {
|
|
212
223
|
const pattern = `%${search}%`;
|
|
224
|
+
const externalIdentifierMatch = sql `EXISTS (
|
|
225
|
+
SELECT 1 FROM ${externalTaskLinks}
|
|
226
|
+
WHERE ${externalTaskLinks.epicId} = ${epics.id}
|
|
227
|
+
AND (lower(${externalTaskLinks.remoteTaskId}) LIKE ${pattern}
|
|
228
|
+
OR lower(${safeJsonText(externalTaskLinks.sourceSnapshot, '$.remoteKey')}) LIKE ${pattern})
|
|
229
|
+
)`;
|
|
213
230
|
const isUuidPrefix = search.length >= 8 && /^[a-f0-9-]+$/.test(search);
|
|
214
231
|
if (isUuidPrefix) {
|
|
215
232
|
const idPrefixPattern = `${search}%`;
|
|
216
|
-
conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern} OR ${epics.id} LIKE ${idPrefixPattern})`);
|
|
233
|
+
conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern} OR ${epics.id} LIKE ${idPrefixPattern} OR ${externalIdentifierMatch})`);
|
|
217
234
|
}
|
|
218
235
|
else {
|
|
219
|
-
conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern})`);
|
|
236
|
+
conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern} OR ${externalIdentifierMatch})`);
|
|
220
237
|
}
|
|
221
238
|
}
|
|
222
239
|
}
|
|
@@ -372,6 +389,12 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
372
389
|
versionOf: (current) => current.version,
|
|
373
390
|
prepare: ({ current }) => {
|
|
374
391
|
if (data.parentId !== undefined) {
|
|
392
|
+
if (data.parentId !== current.parentId) {
|
|
393
|
+
this.ensureEpicIsNotRouteEndpointSync(id);
|
|
394
|
+
}
|
|
395
|
+
if (data.parentId === null && current.parentId !== null) {
|
|
396
|
+
this.assertPromotionKeepsEligibleRoutesSync(id, current.projectId);
|
|
397
|
+
}
|
|
375
398
|
this.ensureValidEpicParentSync(current.projectId, data.parentId ?? null, id);
|
|
376
399
|
}
|
|
377
400
|
if (data.agentId !== undefined) {
|
|
@@ -436,8 +459,14 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
436
459
|
if (rows.length === 0) {
|
|
437
460
|
throw new error_types_1.NotFoundError('Epic', id);
|
|
438
461
|
}
|
|
462
|
+
const { workspace_id: workspaceId } = this.rawClient
|
|
463
|
+
.prepare(`SELECT project.workspace_id
|
|
464
|
+
FROM epics epic
|
|
465
|
+
INNER JOIN projects project ON project.id = epic.project_id
|
|
466
|
+
WHERE epic.id = ?`)
|
|
467
|
+
.get(id);
|
|
439
468
|
for (const row of rows) {
|
|
440
|
-
const event = eventFactory?.(this.getEpicSync(row.id));
|
|
469
|
+
const event = eventFactory?.(this.getEpicSync(row.id), workspaceId);
|
|
441
470
|
if (event) {
|
|
442
471
|
this.dependencies.appendEvent(event);
|
|
443
472
|
}
|
|
@@ -651,27 +680,35 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
651
680
|
offset,
|
|
652
681
|
};
|
|
653
682
|
}
|
|
654
|
-
async createEpicComment(data) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
683
|
+
async createEpicComment(data, eventFactory) {
|
|
684
|
+
return this.txRunner.runImmediateQueued(() => this.insertEpicComment(data, eventFactory));
|
|
685
|
+
}
|
|
686
|
+
insertEpicComment(data, eventFactory) {
|
|
687
|
+
const epic = this.getEpicSync(data.epicId);
|
|
658
688
|
const now = new Date().toISOString();
|
|
659
689
|
const comment = {
|
|
660
|
-
id: randomUUID(),
|
|
690
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
661
691
|
epicId: data.epicId,
|
|
662
692
|
authorName: data.authorName,
|
|
663
693
|
content: data.content,
|
|
664
694
|
createdAt: now,
|
|
665
695
|
updatedAt: now,
|
|
666
696
|
};
|
|
667
|
-
|
|
697
|
+
this.db
|
|
698
|
+
.insert(schema_1.epicComments)
|
|
699
|
+
.values({
|
|
668
700
|
id: comment.id,
|
|
669
701
|
epicId: comment.epicId,
|
|
670
702
|
authorName: comment.authorName,
|
|
671
703
|
content: comment.content,
|
|
672
704
|
createdAt: comment.createdAt,
|
|
673
705
|
updatedAt: comment.updatedAt,
|
|
674
|
-
})
|
|
706
|
+
})
|
|
707
|
+
.run();
|
|
708
|
+
const event = eventFactory?.(comment, epic);
|
|
709
|
+
if (event) {
|
|
710
|
+
this.dependencies.appendEvent(event);
|
|
711
|
+
}
|
|
675
712
|
return comment;
|
|
676
713
|
}
|
|
677
714
|
async deleteEpicComment(id) {
|
|
@@ -695,6 +732,661 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
695
732
|
.from(epics)
|
|
696
733
|
.where(and(eq(epics.projectId, projectId), sql `substr(${epics.id}, 1, ${prefix.length}) = ${prefix}`));
|
|
697
734
|
}
|
|
735
|
+
async setEpicRelation(data, context) {
|
|
736
|
+
return this.txRunner.runImmediateQueuedOrJoin(() => {
|
|
737
|
+
const [focal, related] = this.validateRelationPairSync(data.epicId, data.relatedEpicId);
|
|
738
|
+
this.authorizeRelationWriteSync(focal, related, context);
|
|
739
|
+
const { leftEpicId, rightEpicId } = this.canonicalRelationPair(focal.id, related.id);
|
|
740
|
+
const { type, direction } = this.toStoredRelation(focal.id, leftEpicId, data.type);
|
|
741
|
+
const createdBy = data.createdBy ?? null;
|
|
742
|
+
const createdByAgentId = data.createdByAgentId ?? null;
|
|
743
|
+
if (createdBy !== null && createdBy !== 'user' && createdBy !== 'agent') {
|
|
744
|
+
throw new error_types_1.ValidationError('Relation creator type must be user or agent.');
|
|
745
|
+
}
|
|
746
|
+
if (createdByAgentId !== null) {
|
|
747
|
+
if (createdBy !== 'agent') {
|
|
748
|
+
throw new error_types_1.ValidationError('An agent relation creator requires createdBy="agent".');
|
|
749
|
+
}
|
|
750
|
+
const creator = this.dependencies.getAgent(createdByAgentId);
|
|
751
|
+
if (creator.projectId !== focal.project_id) {
|
|
752
|
+
throw new error_types_1.ValidationError('Relation creator must belong to the focal Epic project.', {
|
|
753
|
+
createdByAgentId,
|
|
754
|
+
projectId: focal.project_id,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const existing = this.rawClient
|
|
759
|
+
.prepare(`SELECT id, left_epic_id, right_epic_id, type, direction,
|
|
760
|
+
created_by, created_by_agent_id, created_at, updated_at
|
|
761
|
+
FROM epic_relations
|
|
762
|
+
WHERE left_epic_id = ? AND right_epic_id = ?`)
|
|
763
|
+
.get(leftEpicId, rightEpicId);
|
|
764
|
+
if (existing?.type === type && existing.direction === direction) {
|
|
765
|
+
return {
|
|
766
|
+
...this.mapEpicRelation(existing),
|
|
767
|
+
changed: false,
|
|
768
|
+
workspaceId: focal.workspace_id,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const pairEligible = focal.parent_id === null &&
|
|
772
|
+
related.parent_id === null &&
|
|
773
|
+
focal.project_id === related.project_id;
|
|
774
|
+
const directedEffect = type === 'related' ? (0, domain_models_1.resolveRelationEndpoints)(leftEpicId, rightEpicId, direction) : null;
|
|
775
|
+
const newEligibleRoute = pairEligible ? directedEffect : null;
|
|
776
|
+
const oldEffect = existing && existing.type === 'related' && existing.direction !== 'none' && pairEligible
|
|
777
|
+
? (0, domain_models_1.resolveRelationEndpoints)(existing.left_epic_id, existing.right_epic_id, existing.direction)
|
|
778
|
+
: null;
|
|
779
|
+
const effectUnchanged = newEligibleRoute !== null &&
|
|
780
|
+
oldEffect !== null &&
|
|
781
|
+
newEligibleRoute.sourceEpicId === oldEffect.sourceEpicId &&
|
|
782
|
+
newEligibleRoute.targetEpicId === oldEffect.targetEpicId;
|
|
783
|
+
const displacedEffects = [];
|
|
784
|
+
if (oldEffect !== null && !effectUnchanged) {
|
|
785
|
+
displacedEffects.push(oldEffect);
|
|
786
|
+
}
|
|
787
|
+
if (newEligibleRoute) {
|
|
788
|
+
const otherRoute = this.findOutgoingEligibleRouteSync(newEligibleRoute.sourceEpicId, existing?.id);
|
|
789
|
+
if (otherRoute) {
|
|
790
|
+
displacedEffects.push(otherRoute);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (displacedEffects.length > 1) {
|
|
794
|
+
throw new error_types_1.ValidationError('This change would displace two active time routes. Delete one Related pair explicitly first, then retry the change.', { displacedEffects });
|
|
795
|
+
}
|
|
796
|
+
const displacedEffect = displacedEffects[0] ?? null;
|
|
797
|
+
if (displacedEffect) {
|
|
798
|
+
const accepted = data.acceptedRouteEffect;
|
|
799
|
+
if (context.trustedLocalHuman !== true ||
|
|
800
|
+
!accepted ||
|
|
801
|
+
accepted.sourceEpicId !== displacedEffect.sourceEpicId ||
|
|
802
|
+
accepted.targetEpicId !== displacedEffect.targetEpicId) {
|
|
803
|
+
throw new error_types_1.RelationConfirmationRequiredError(displacedEffect);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (newEligibleRoute) {
|
|
807
|
+
this.assertNoEligibleRouteCycleSync(newEligibleRoute, existing?.id);
|
|
808
|
+
}
|
|
809
|
+
const id = (0, node_crypto_1.randomUUID)();
|
|
810
|
+
const now = new Date().toISOString();
|
|
811
|
+
this.rawClient
|
|
812
|
+
.prepare(`INSERT INTO epic_relations (
|
|
813
|
+
id, left_epic_id, right_epic_id, type, direction,
|
|
814
|
+
created_by, created_by_agent_id, created_at, updated_at
|
|
815
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
816
|
+
ON CONFLICT(left_epic_id, right_epic_id) DO UPDATE SET
|
|
817
|
+
type = excluded.type,
|
|
818
|
+
direction = excluded.direction,
|
|
819
|
+
updated_at = excluded.updated_at`)
|
|
820
|
+
.run(id, leftEpicId, rightEpicId, type, direction, createdBy, createdByAgentId, now, now);
|
|
821
|
+
if (newEligibleRoute) {
|
|
822
|
+
this.deleteOutgoingEligibleRouteSync(newEligibleRoute.sourceEpicId, leftEpicId, rightEpicId);
|
|
823
|
+
}
|
|
824
|
+
const row = this.rawClient
|
|
825
|
+
.prepare(`SELECT id, left_epic_id, right_epic_id, type, direction,
|
|
826
|
+
created_by, created_by_agent_id, created_at, updated_at
|
|
827
|
+
FROM epic_relations
|
|
828
|
+
WHERE left_epic_id = ? AND right_epic_id = ?`)
|
|
829
|
+
.get(leftEpicId, rightEpicId);
|
|
830
|
+
return {
|
|
831
|
+
...this.mapEpicRelation(row),
|
|
832
|
+
changed: true,
|
|
833
|
+
workspaceId: focal.workspace_id,
|
|
834
|
+
};
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
async deleteEpicRelation(epicId, relatedEpicId, context) {
|
|
838
|
+
return this.txRunner.runImmediateQueuedOrJoin(() => {
|
|
839
|
+
const [focal, related] = this.validateRelationPairSync(epicId, relatedEpicId);
|
|
840
|
+
this.authorizeRelationWriteSync(focal, related, context);
|
|
841
|
+
const pair = this.canonicalRelationPair(focal.id, related.id);
|
|
842
|
+
const deleted = this.rawClient
|
|
843
|
+
.prepare('DELETE FROM epic_relations WHERE left_epic_id = ? AND right_epic_id = ?')
|
|
844
|
+
.run(pair.leftEpicId, pair.rightEpicId).changes > 0;
|
|
845
|
+
return { deleted, workspaceId: focal.workspace_id };
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async listEpicRelations(epicId, options = {}) {
|
|
849
|
+
const focal = this.getRelationContextSync(epicId);
|
|
850
|
+
this.assertRelationReadWorkspace(focal, options);
|
|
851
|
+
const limit = this.normalizeRelationListLimit(options.limit, 'Relation');
|
|
852
|
+
const offset = this.normalizeRelationListOffset(options.offset, 'Relation');
|
|
853
|
+
const relatedEpicId = options.relatedEpicId ?? null;
|
|
854
|
+
const queryArgs = [
|
|
855
|
+
epicId,
|
|
856
|
+
epicId,
|
|
857
|
+
epicId,
|
|
858
|
+
options.excludeMcpHidden ? 1 : 0,
|
|
859
|
+
relatedEpicId,
|
|
860
|
+
relatedEpicId,
|
|
861
|
+
];
|
|
862
|
+
const count = this.rawClient
|
|
863
|
+
.prepare(`SELECT COUNT(*) AS total
|
|
864
|
+
FROM epic_relations relation
|
|
865
|
+
INNER JOIN epics target
|
|
866
|
+
ON target.id = CASE
|
|
867
|
+
WHEN relation.left_epic_id = ? THEN relation.right_epic_id
|
|
868
|
+
ELSE relation.left_epic_id
|
|
869
|
+
END
|
|
870
|
+
INNER JOIN statuses status ON status.id = target.status_id
|
|
871
|
+
WHERE (relation.left_epic_id = ? OR relation.right_epic_id = ?)
|
|
872
|
+
AND (? = 0 OR status.mcp_hidden = 0)
|
|
873
|
+
AND (? IS NULL OR target.id = ?)`)
|
|
874
|
+
.get(...queryArgs);
|
|
875
|
+
const rows = this.rawClient
|
|
876
|
+
.prepare(`SELECT
|
|
877
|
+
relation.id AS relation_id,
|
|
878
|
+
relation.left_epic_id,
|
|
879
|
+
relation.right_epic_id,
|
|
880
|
+
relation.type,
|
|
881
|
+
relation.direction,
|
|
882
|
+
relation.created_at,
|
|
883
|
+
relation.updated_at,
|
|
884
|
+
target.id AS epic_id,
|
|
885
|
+
target.project_id,
|
|
886
|
+
project.name AS project_name,
|
|
887
|
+
target.title,
|
|
888
|
+
target.status_id,
|
|
889
|
+
status.label AS status_label,
|
|
890
|
+
status.color AS status_color,
|
|
891
|
+
status.mcp_hidden AS status_mcp_hidden
|
|
892
|
+
FROM epic_relations relation
|
|
893
|
+
INNER JOIN epics target
|
|
894
|
+
ON target.id = CASE
|
|
895
|
+
WHEN relation.left_epic_id = ? THEN relation.right_epic_id
|
|
896
|
+
ELSE relation.left_epic_id
|
|
897
|
+
END
|
|
898
|
+
INNER JOIN projects project ON project.id = target.project_id
|
|
899
|
+
INNER JOIN statuses status ON status.id = target.status_id
|
|
900
|
+
WHERE (relation.left_epic_id = ? OR relation.right_epic_id = ?)
|
|
901
|
+
AND (? = 0 OR status.mcp_hidden = 0)
|
|
902
|
+
AND (? IS NULL OR target.id = ?)
|
|
903
|
+
ORDER BY lower(project.name), lower(target.title), target.id
|
|
904
|
+
LIMIT ? OFFSET ?`)
|
|
905
|
+
.all(...queryArgs, limit, offset);
|
|
906
|
+
return {
|
|
907
|
+
items: rows.map((row) => {
|
|
908
|
+
const endpoints = (0, domain_models_1.resolveRelationEndpoints)(row.left_epic_id, row.right_epic_id, row.direction);
|
|
909
|
+
return {
|
|
910
|
+
relationId: row.relation_id,
|
|
911
|
+
epicId: row.epic_id,
|
|
912
|
+
projectId: row.project_id,
|
|
913
|
+
projectName: row.project_name,
|
|
914
|
+
title: row.title,
|
|
915
|
+
statusId: row.status_id,
|
|
916
|
+
statusLabel: row.status_label,
|
|
917
|
+
statusColor: row.status_color,
|
|
918
|
+
statusMcpHidden: Boolean(row.status_mcp_hidden),
|
|
919
|
+
type: this.toFocalRelationType(epicId, row),
|
|
920
|
+
sourceEpicId: endpoints?.sourceEpicId ?? null,
|
|
921
|
+
targetEpicId: endpoints?.targetEpicId ?? null,
|
|
922
|
+
createdAt: row.created_at,
|
|
923
|
+
updatedAt: row.updated_at,
|
|
924
|
+
};
|
|
925
|
+
}),
|
|
926
|
+
total: count.total,
|
|
927
|
+
limit,
|
|
928
|
+
offset,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async summarizeEpicRelationsBatch(epicIds, options = {}) {
|
|
932
|
+
if (!Array.isArray(epicIds)) {
|
|
933
|
+
throw new error_types_1.ValidationError('epicIds must be an array.');
|
|
934
|
+
}
|
|
935
|
+
const uniqueIds = Array.from(new Set(epicIds.map((id) => (typeof id === 'string' ? id.trim() : ''))));
|
|
936
|
+
if (uniqueIds.some((id) => id.length === 0)) {
|
|
937
|
+
throw new error_types_1.ValidationError('epicIds must contain non-empty strings.');
|
|
938
|
+
}
|
|
939
|
+
if (uniqueIds.length > MAX_RELATION_BATCH_SIZE) {
|
|
940
|
+
throw new error_types_1.ValidationError(`epicIds cannot contain more than ${MAX_RELATION_BATCH_SIZE} IDs.`);
|
|
941
|
+
}
|
|
942
|
+
if (uniqueIds.length === 0)
|
|
943
|
+
return new Map();
|
|
944
|
+
const rows = this.rawClient
|
|
945
|
+
.prepare(`WITH requested(epic_id) AS (
|
|
946
|
+
SELECT value FROM json_each(?)
|
|
947
|
+
), focal_relations AS (
|
|
948
|
+
SELECT relation.left_epic_id AS epic_id,
|
|
949
|
+
relation.right_epic_id AS target_epic_id,
|
|
950
|
+
relation.type, relation.direction, 1 AS focal_is_left
|
|
951
|
+
FROM epic_relations relation
|
|
952
|
+
UNION ALL
|
|
953
|
+
SELECT relation.right_epic_id AS epic_id,
|
|
954
|
+
relation.left_epic_id AS target_epic_id,
|
|
955
|
+
relation.type, relation.direction, 0 AS focal_is_left
|
|
956
|
+
FROM epic_relations relation
|
|
957
|
+
)
|
|
958
|
+
SELECT
|
|
959
|
+
focal.epic_id,
|
|
960
|
+
SUM(CASE WHEN focal.type = 'related' THEN 1 ELSE 0 END) AS related,
|
|
961
|
+
SUM(CASE WHEN focal.type = 'related' AND (
|
|
962
|
+
(focal.focal_is_left = 1 AND focal.direction = 'left_to_right') OR
|
|
963
|
+
(focal.focal_is_left = 0 AND focal.direction = 'right_to_left')
|
|
964
|
+
) THEN 1 ELSE 0 END) AS related_targets,
|
|
965
|
+
SUM(CASE WHEN focal.type = 'related' AND (
|
|
966
|
+
(focal.focal_is_left = 1 AND focal.direction = 'right_to_left') OR
|
|
967
|
+
(focal.focal_is_left = 0 AND focal.direction = 'left_to_right')
|
|
968
|
+
) THEN 1 ELSE 0 END) AS related_sources,
|
|
969
|
+
SUM(CASE WHEN focal.type = 'related' AND focal.direction = 'none' THEN 1 ELSE 0 END) AS related_neutral,
|
|
970
|
+
SUM(CASE WHEN focal.type = 'blocks' AND (
|
|
971
|
+
(focal.focal_is_left = 1 AND focal.direction = 'left_to_right') OR
|
|
972
|
+
(focal.focal_is_left = 0 AND focal.direction = 'right_to_left')
|
|
973
|
+
) THEN 1 ELSE 0 END) AS blocks,
|
|
974
|
+
SUM(CASE WHEN focal.type = 'blocks' AND (
|
|
975
|
+
(focal.focal_is_left = 1 AND focal.direction = 'right_to_left') OR
|
|
976
|
+
(focal.focal_is_left = 0 AND focal.direction = 'left_to_right')
|
|
977
|
+
) THEN 1 ELSE 0 END) AS blocked_by,
|
|
978
|
+
COUNT(*) AS total
|
|
979
|
+
FROM requested
|
|
980
|
+
INNER JOIN focal_relations focal ON focal.epic_id = requested.epic_id
|
|
981
|
+
INNER JOIN epics target ON target.id = focal.target_epic_id
|
|
982
|
+
INNER JOIN statuses target_status ON target_status.id = target.status_id
|
|
983
|
+
INNER JOIN epics focal_epic ON focal_epic.id = focal.epic_id
|
|
984
|
+
INNER JOIN projects focal_project ON focal_project.id = focal_epic.project_id
|
|
985
|
+
WHERE (? = 0 OR target_status.mcp_hidden = 0)
|
|
986
|
+
AND (? IS NULL OR focal_project.workspace_id = ?)
|
|
987
|
+
GROUP BY focal.epic_id`)
|
|
988
|
+
.all(JSON.stringify(uniqueIds), options.excludeMcpHidden ? 1 : 0, options.workspaceId ?? null, options.workspaceId ?? null);
|
|
989
|
+
return new Map(rows.map((row) => [
|
|
990
|
+
row.epic_id,
|
|
991
|
+
{
|
|
992
|
+
epicId: row.epic_id,
|
|
993
|
+
related: Number(row.related),
|
|
994
|
+
relatedTargets: Number(row.related_targets),
|
|
995
|
+
relatedSources: Number(row.related_sources),
|
|
996
|
+
relatedNeutral: Number(row.related_neutral),
|
|
997
|
+
blocks: Number(row.blocks),
|
|
998
|
+
blockedBy: Number(row.blocked_by),
|
|
999
|
+
total: Number(row.total),
|
|
1000
|
+
},
|
|
1001
|
+
]));
|
|
1002
|
+
}
|
|
1003
|
+
async listEpicRelationCandidates(epicId, options = {}) {
|
|
1004
|
+
const focal = this.getRelationContextSync(epicId);
|
|
1005
|
+
this.assertRelationReadWorkspace(focal, options);
|
|
1006
|
+
const limit = this.normalizeRelationListLimit(options.limit, 'Relation candidate');
|
|
1007
|
+
const offset = this.normalizeRelationListOffset(options.offset, 'Relation candidate');
|
|
1008
|
+
const search = typeof options.q === 'string' ? options.q.trim().toLowerCase() : '';
|
|
1009
|
+
if (search.length > 200) {
|
|
1010
|
+
throw new error_types_1.ValidationError('Relation candidate search cannot exceed 200 characters.');
|
|
1011
|
+
}
|
|
1012
|
+
const conditions = [
|
|
1013
|
+
'project.workspace_id = ?',
|
|
1014
|
+
'project.is_template = 0',
|
|
1015
|
+
'(? = 0 OR status.mcp_hidden = 0)',
|
|
1016
|
+
'candidate.id <> ?',
|
|
1017
|
+
'(candidate.parent_id IS NULL OR candidate.parent_id <> ?)',
|
|
1018
|
+
'(? IS NULL OR candidate.id <> ?)',
|
|
1019
|
+
`NOT EXISTS (
|
|
1020
|
+
SELECT 1 FROM epic_relations existing
|
|
1021
|
+
WHERE (existing.left_epic_id = ? AND existing.right_epic_id = candidate.id)
|
|
1022
|
+
OR (existing.right_epic_id = ? AND existing.left_epic_id = candidate.id)
|
|
1023
|
+
)`,
|
|
1024
|
+
];
|
|
1025
|
+
const parameters = [
|
|
1026
|
+
focal.workspace_id,
|
|
1027
|
+
options.excludeMcpHidden ? 1 : 0,
|
|
1028
|
+
focal.id,
|
|
1029
|
+
focal.id,
|
|
1030
|
+
focal.parent_id,
|
|
1031
|
+
focal.parent_id,
|
|
1032
|
+
focal.id,
|
|
1033
|
+
focal.id,
|
|
1034
|
+
];
|
|
1035
|
+
if (search) {
|
|
1036
|
+
const escapedSearch = search.replace(/[\\%_]/g, '\\$&');
|
|
1037
|
+
conditions.push(`(lower(candidate.title) LIKE ? ESCAPE '\\'
|
|
1038
|
+
OR lower(substr(candidate.id, 1, ?)) = ?)`);
|
|
1039
|
+
parameters.push(`%${escapedSearch}%`, search.length, search);
|
|
1040
|
+
}
|
|
1041
|
+
const where = conditions.join('\n AND ');
|
|
1042
|
+
const totalRow = this.rawClient
|
|
1043
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
1044
|
+
FROM epics candidate
|
|
1045
|
+
INNER JOIN projects project ON project.id = candidate.project_id
|
|
1046
|
+
INNER JOIN statuses status ON status.id = candidate.status_id
|
|
1047
|
+
WHERE ${where}`)
|
|
1048
|
+
.get(...parameters);
|
|
1049
|
+
const rows = this.rawClient
|
|
1050
|
+
.prepare(`SELECT candidate.id, candidate.project_id, project.name AS project_name,
|
|
1051
|
+
candidate.title, candidate.status_id, status.label AS status_label,
|
|
1052
|
+
status.color AS status_color, status.mcp_hidden AS status_mcp_hidden,
|
|
1053
|
+
candidate.parent_id
|
|
1054
|
+
FROM epics candidate
|
|
1055
|
+
INNER JOIN projects project ON project.id = candidate.project_id
|
|
1056
|
+
INNER JOIN statuses status ON status.id = candidate.status_id
|
|
1057
|
+
WHERE ${where}
|
|
1058
|
+
ORDER BY lower(project.name), lower(candidate.title), candidate.id
|
|
1059
|
+
LIMIT ? OFFSET ?`)
|
|
1060
|
+
.all(...parameters, limit, offset);
|
|
1061
|
+
return {
|
|
1062
|
+
items: rows.map((row) => this.mapRelationCandidate(row)),
|
|
1063
|
+
total: Number(totalRow.count),
|
|
1064
|
+
limit,
|
|
1065
|
+
offset,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async getWorkspaceEpicsByIdPrefix(epicId, prefix, options = {}) {
|
|
1069
|
+
const focal = this.getRelationContextSync(epicId);
|
|
1070
|
+
this.assertRelationReadWorkspace(focal, options);
|
|
1071
|
+
const normalizedPrefix = typeof prefix === 'string' ? prefix.trim().toLowerCase() : '';
|
|
1072
|
+
if (!/^[a-f0-9-]{8,36}$/.test(normalizedPrefix)) {
|
|
1073
|
+
return [];
|
|
1074
|
+
}
|
|
1075
|
+
const rows = this.rawClient
|
|
1076
|
+
.prepare(`SELECT candidate.id, candidate.project_id, project.name AS project_name,
|
|
1077
|
+
candidate.title, candidate.status_id, status.label AS status_label,
|
|
1078
|
+
status.color AS status_color, status.mcp_hidden AS status_mcp_hidden,
|
|
1079
|
+
candidate.parent_id
|
|
1080
|
+
FROM epics candidate
|
|
1081
|
+
INNER JOIN projects project ON project.id = candidate.project_id
|
|
1082
|
+
INNER JOIN statuses status ON status.id = candidate.status_id
|
|
1083
|
+
WHERE project.workspace_id = ?
|
|
1084
|
+
AND project.is_template = 0
|
|
1085
|
+
AND (? = 0 OR status.mcp_hidden = 0)
|
|
1086
|
+
AND candidate.id <> ?
|
|
1087
|
+
AND lower(substr(candidate.id, 1, ?)) = ?
|
|
1088
|
+
ORDER BY candidate.id
|
|
1089
|
+
LIMIT ?`)
|
|
1090
|
+
.all(focal.workspace_id, options.excludeMcpHidden ? 1 : 0, focal.id, normalizedPrefix.length, normalizedPrefix, MAX_RELATION_PREFIX_RESULTS);
|
|
1091
|
+
return rows.map((row) => this.mapRelationCandidate(row));
|
|
1092
|
+
}
|
|
1093
|
+
validateRelationPairSync(epicId, relatedEpicId) {
|
|
1094
|
+
if (typeof epicId !== 'string' || typeof relatedEpicId !== 'string') {
|
|
1095
|
+
throw new error_types_1.ValidationError('Relation Epic IDs must be strings.');
|
|
1096
|
+
}
|
|
1097
|
+
if (epicId === relatedEpicId) {
|
|
1098
|
+
throw new error_types_1.ValidationError('An Epic cannot have a relation to itself.', { epicId });
|
|
1099
|
+
}
|
|
1100
|
+
const focal = this.getRelationContextSync(epicId);
|
|
1101
|
+
let related;
|
|
1102
|
+
try {
|
|
1103
|
+
related = this.getRelationContextSync(relatedEpicId);
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
if (error instanceof error_types_1.NotFoundError) {
|
|
1107
|
+
throw new error_types_1.NotFoundError('Related Epic');
|
|
1108
|
+
}
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
if (focal.workspace_id !== related.workspace_id) {
|
|
1112
|
+
throw new error_types_1.NotFoundError('Related Epic');
|
|
1113
|
+
}
|
|
1114
|
+
if (focal.parent_id === related.id || related.parent_id === focal.id) {
|
|
1115
|
+
throw new error_types_1.ValidationError('A parent and direct sub-Epic cannot have a relation.', {
|
|
1116
|
+
epicId,
|
|
1117
|
+
relatedEpicId,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
return [focal, related];
|
|
1121
|
+
}
|
|
1122
|
+
authorizeRelationWriteSync(focal, related, context) {
|
|
1123
|
+
if (context?.actor?.type === 'guest') {
|
|
1124
|
+
throw new error_types_1.ForbiddenError('Guests cannot write Epic relations.');
|
|
1125
|
+
}
|
|
1126
|
+
if (context?.trustedLocalHuman === true && !context.actor) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (context?.actor?.type !== 'agent') {
|
|
1130
|
+
throw new error_types_1.ForbiddenError('An authorized relation writer is required.');
|
|
1131
|
+
}
|
|
1132
|
+
const currentAgent = this.rawClient
|
|
1133
|
+
.prepare('SELECT project_id, is_project_owner FROM agents WHERE id = ?')
|
|
1134
|
+
.get(context.actor.id);
|
|
1135
|
+
if (!currentAgent || currentAgent.project_id !== focal.project_id) {
|
|
1136
|
+
throw new error_types_1.ForbiddenError('The caller must currently belong to the focal Epic project.');
|
|
1137
|
+
}
|
|
1138
|
+
if (related.project_id !== focal.project_id && !Boolean(currentAgent.is_project_owner)) {
|
|
1139
|
+
throw new error_types_1.ForbiddenError('Cross-project Epic relations require the focal project current Project Owner.');
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
assertRelationReadWorkspace(focal, options) {
|
|
1143
|
+
if (options.workspaceId && focal.workspace_id !== options.workspaceId) {
|
|
1144
|
+
throw new error_types_1.NotFoundError('Epic');
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
getRelationContextSync(epicId) {
|
|
1148
|
+
const row = this.rawClient
|
|
1149
|
+
.prepare(`SELECT epic.id, epic.project_id, project.workspace_id, epic.parent_id
|
|
1150
|
+
FROM epics epic
|
|
1151
|
+
INNER JOIN projects project ON project.id = epic.project_id
|
|
1152
|
+
WHERE epic.id = ?`)
|
|
1153
|
+
.get(epicId);
|
|
1154
|
+
if (!row) {
|
|
1155
|
+
throw new error_types_1.NotFoundError('Epic', epicId);
|
|
1156
|
+
}
|
|
1157
|
+
return row;
|
|
1158
|
+
}
|
|
1159
|
+
canonicalRelationPair(epicId, relatedEpicId) {
|
|
1160
|
+
return epicId < relatedEpicId
|
|
1161
|
+
? { leftEpicId: epicId, rightEpicId: relatedEpicId }
|
|
1162
|
+
: { leftEpicId: relatedEpicId, rightEpicId: epicId };
|
|
1163
|
+
}
|
|
1164
|
+
toStoredRelation(focalEpicId, leftEpicId, focalType) {
|
|
1165
|
+
const focalIsLeft = focalEpicId === leftEpicId;
|
|
1166
|
+
if (focalType === 'related') {
|
|
1167
|
+
return { type: 'related', direction: focalIsLeft ? 'left_to_right' : 'right_to_left' };
|
|
1168
|
+
}
|
|
1169
|
+
if (focalType !== 'blocks' && focalType !== 'blocked_by') {
|
|
1170
|
+
throw new error_types_1.ValidationError('Relation type must be related, blocks, or blocked_by.');
|
|
1171
|
+
}
|
|
1172
|
+
const leftBlocksRight = focalType === 'blocks' ? focalIsLeft : !focalIsLeft;
|
|
1173
|
+
return {
|
|
1174
|
+
type: 'blocks',
|
|
1175
|
+
direction: leftBlocksRight ? 'left_to_right' : 'right_to_left',
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
static ELIGIBLE_ROUTE_SOURCE_PREDICATE = `
|
|
1179
|
+
relation.type = 'related'
|
|
1180
|
+
AND relation.direction != 'none'
|
|
1181
|
+
AND left_epic.parent_id IS NULL
|
|
1182
|
+
AND right_epic.parent_id IS NULL
|
|
1183
|
+
AND left_epic.project_id = right_epic.project_id
|
|
1184
|
+
AND (
|
|
1185
|
+
(relation.left_epic_id = ? AND relation.direction = 'left_to_right')
|
|
1186
|
+
OR (relation.right_epic_id = ? AND relation.direction = 'right_to_left')
|
|
1187
|
+
)`;
|
|
1188
|
+
assertNoEligibleRouteCycleSync(newEffect, replacedRowId) {
|
|
1189
|
+
const outgoing = this.rawClient.prepare(`SELECT relation.id, relation.left_epic_id, relation.right_epic_id, relation.direction
|
|
1190
|
+
FROM epic_relations relation
|
|
1191
|
+
INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
|
|
1192
|
+
INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
|
|
1193
|
+
WHERE${EpicStorageDelegate.ELIGIBLE_ROUTE_SOURCE_PREDICATE}`);
|
|
1194
|
+
const visited = new Set([newEffect.targetEpicId]);
|
|
1195
|
+
let current = newEffect.targetEpicId;
|
|
1196
|
+
for (let step = 0; step <= MAX_RELATION_BATCH_SIZE; step++) {
|
|
1197
|
+
if (current === newEffect.sourceEpicId) {
|
|
1198
|
+
throw new error_types_1.ValidationError('A time route cannot form a cycle.', {
|
|
1199
|
+
sourceEpicId: newEffect.sourceEpicId,
|
|
1200
|
+
targetEpicId: newEffect.targetEpicId,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
const edge = outgoing.get(current, current);
|
|
1204
|
+
if (!edge || edge.id === replacedRowId) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const effect = (0, domain_models_1.resolveRelationEndpoints)(edge.left_epic_id, edge.right_epic_id, edge.direction);
|
|
1208
|
+
if (!effect || visited.has(effect.targetEpicId)) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
visited.add(effect.targetEpicId);
|
|
1212
|
+
current = effect.targetEpicId;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
findOutgoingEligibleRouteSync(sourceEpicId, excludedRowId) {
|
|
1216
|
+
const row = this.rawClient
|
|
1217
|
+
.prepare(`SELECT relation.id, relation.left_epic_id, relation.right_epic_id, relation.direction
|
|
1218
|
+
FROM epic_relations relation
|
|
1219
|
+
INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
|
|
1220
|
+
INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
|
|
1221
|
+
WHERE${EpicStorageDelegate.ELIGIBLE_ROUTE_SOURCE_PREDICATE}
|
|
1222
|
+
LIMIT 1`)
|
|
1223
|
+
.get(sourceEpicId, sourceEpicId);
|
|
1224
|
+
if (!row || row.id === excludedRowId) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
return (0, domain_models_1.resolveRelationEndpoints)(row.left_epic_id, row.right_epic_id, row.direction);
|
|
1228
|
+
}
|
|
1229
|
+
deleteOutgoingEligibleRouteSync(sourceEpicId, keepLeftEpicId, keepRightEpicId) {
|
|
1230
|
+
this.rawClient
|
|
1231
|
+
.prepare(`DELETE FROM epic_relations
|
|
1232
|
+
WHERE id IN (
|
|
1233
|
+
SELECT relation.id
|
|
1234
|
+
FROM epic_relations relation
|
|
1235
|
+
INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
|
|
1236
|
+
INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
|
|
1237
|
+
WHERE${EpicStorageDelegate.ELIGIBLE_ROUTE_SOURCE_PREDICATE}
|
|
1238
|
+
)
|
|
1239
|
+
AND NOT (left_epic_id = ? AND right_epic_id = ?)`)
|
|
1240
|
+
.run(sourceEpicId, sourceEpicId, keepLeftEpicId, keepRightEpicId);
|
|
1241
|
+
}
|
|
1242
|
+
ensureEpicIsNotRouteEndpointSync(epicId) {
|
|
1243
|
+
const routed = this.rawClient
|
|
1244
|
+
.prepare(`SELECT relation.left_epic_id, relation.right_epic_id
|
|
1245
|
+
FROM epic_relations relation
|
|
1246
|
+
INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
|
|
1247
|
+
INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
|
|
1248
|
+
WHERE relation.type = 'related'
|
|
1249
|
+
AND relation.direction != 'none'
|
|
1250
|
+
AND left_epic.parent_id IS NULL
|
|
1251
|
+
AND right_epic.parent_id IS NULL
|
|
1252
|
+
AND left_epic.project_id = right_epic.project_id
|
|
1253
|
+
AND (relation.left_epic_id = ? OR relation.right_epic_id = ?)
|
|
1254
|
+
LIMIT 1`)
|
|
1255
|
+
.get(epicId, epicId);
|
|
1256
|
+
if (routed) {
|
|
1257
|
+
throw new error_types_1.ValidationError('Delete the eligible Related pair explicitly before reparenting one of its endpoints.', { epicId, leftEpicId: routed.left_epic_id, rightEpicId: routed.right_epic_id });
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
assertPromotionKeepsEligibleRoutesSync(epicId, projectId) {
|
|
1261
|
+
const newlyEligible = this.rawClient
|
|
1262
|
+
.prepare(`SELECT CASE WHEN relation.direction = 'left_to_right'
|
|
1263
|
+
THEN relation.left_epic_id ELSE relation.right_epic_id END AS source_id,
|
|
1264
|
+
CASE WHEN relation.direction = 'left_to_right'
|
|
1265
|
+
THEN relation.right_epic_id ELSE relation.left_epic_id END AS target_id
|
|
1266
|
+
FROM epic_relations relation
|
|
1267
|
+
INNER JOIN epics other
|
|
1268
|
+
ON other.id = CASE
|
|
1269
|
+
WHEN relation.left_epic_id = ? THEN relation.right_epic_id
|
|
1270
|
+
ELSE relation.left_epic_id END
|
|
1271
|
+
WHERE relation.type = 'related'
|
|
1272
|
+
AND relation.direction != 'none'
|
|
1273
|
+
AND (relation.left_epic_id = ? OR relation.right_epic_id = ?)
|
|
1274
|
+
AND other.parent_id IS NULL
|
|
1275
|
+
AND other.project_id = ?`)
|
|
1276
|
+
.all(epicId, epicId, epicId, projectId);
|
|
1277
|
+
if (newlyEligible.length === 0) {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const duplicateSource = this.rawClient
|
|
1281
|
+
.prepare(`SELECT CASE WHEN relation.direction = 'left_to_right'
|
|
1282
|
+
THEN relation.left_epic_id ELSE relation.right_epic_id END AS source_id,
|
|
1283
|
+
COUNT(*) AS route_count
|
|
1284
|
+
FROM epic_relations relation
|
|
1285
|
+
INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
|
|
1286
|
+
INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
|
|
1287
|
+
WHERE relation.type = 'related'
|
|
1288
|
+
AND relation.direction != 'none'
|
|
1289
|
+
AND (left_epic.parent_id IS NULL OR left_epic.id = ?)
|
|
1290
|
+
AND (right_epic.parent_id IS NULL OR right_epic.id = ?)
|
|
1291
|
+
AND left_epic.project_id = right_epic.project_id
|
|
1292
|
+
GROUP BY source_id
|
|
1293
|
+
HAVING COUNT(*) > 1
|
|
1294
|
+
LIMIT 1`)
|
|
1295
|
+
.get(epicId, epicId);
|
|
1296
|
+
if (duplicateSource) {
|
|
1297
|
+
throw new error_types_1.ValidationError('Promoting this Epic would activate more than one eligible time route from one source. Delete the extra Related pair explicitly first.', {
|
|
1298
|
+
epicId,
|
|
1299
|
+
sourceEpicId: duplicateSource.source_id,
|
|
1300
|
+
routeCount: duplicateSource.route_count,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
const outgoing = this.rawClient.prepare(`SELECT relation.left_epic_id, relation.right_epic_id, relation.direction
|
|
1304
|
+
FROM epic_relations relation
|
|
1305
|
+
INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
|
|
1306
|
+
INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
|
|
1307
|
+
WHERE relation.type = 'related'
|
|
1308
|
+
AND relation.direction != 'none'
|
|
1309
|
+
AND (left_epic.parent_id IS NULL OR left_epic.id = ?)
|
|
1310
|
+
AND (right_epic.parent_id IS NULL OR right_epic.id = ?)
|
|
1311
|
+
AND left_epic.project_id = right_epic.project_id
|
|
1312
|
+
AND (
|
|
1313
|
+
(relation.left_epic_id = ? AND relation.direction = 'left_to_right')
|
|
1314
|
+
OR (relation.right_epic_id = ? AND relation.direction = 'right_to_left')
|
|
1315
|
+
)`);
|
|
1316
|
+
for (const edge of newlyEligible) {
|
|
1317
|
+
const visited = new Set([edge.target_id]);
|
|
1318
|
+
let current = edge.target_id;
|
|
1319
|
+
for (let step = 0; step <= MAX_RELATION_BATCH_SIZE; step++) {
|
|
1320
|
+
if (current === edge.source_id) {
|
|
1321
|
+
throw new error_types_1.ValidationError('Promoting this Epic would complete a time-route cycle. Delete one Related pair explicitly first.', { epicId, sourceEpicId: edge.source_id, targetEpicId: edge.target_id });
|
|
1322
|
+
}
|
|
1323
|
+
const next = outgoing.get(epicId, epicId, current, current);
|
|
1324
|
+
if (!next) {
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
const effect = (0, domain_models_1.resolveRelationEndpoints)(next.left_epic_id, next.right_epic_id, next.direction);
|
|
1328
|
+
if (!effect || visited.has(effect.targetEpicId)) {
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
visited.add(effect.targetEpicId);
|
|
1332
|
+
current = effect.targetEpicId;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
toFocalRelationType(focalEpicId, relation) {
|
|
1337
|
+
if (relation.type === 'related') {
|
|
1338
|
+
return 'related';
|
|
1339
|
+
}
|
|
1340
|
+
const focalIsLeft = focalEpicId === relation.left_epic_id;
|
|
1341
|
+
const focalBlocks = (focalIsLeft && relation.direction === 'left_to_right') ||
|
|
1342
|
+
(!focalIsLeft && relation.direction === 'right_to_left');
|
|
1343
|
+
return focalBlocks ? 'blocks' : 'blocked_by';
|
|
1344
|
+
}
|
|
1345
|
+
mapEpicRelation(row) {
|
|
1346
|
+
const endpoints = (0, domain_models_1.resolveRelationEndpoints)(row.left_epic_id, row.right_epic_id, row.direction);
|
|
1347
|
+
return {
|
|
1348
|
+
id: row.id,
|
|
1349
|
+
leftEpicId: row.left_epic_id,
|
|
1350
|
+
rightEpicId: row.right_epic_id,
|
|
1351
|
+
type: row.type,
|
|
1352
|
+
direction: row.direction,
|
|
1353
|
+
sourceEpicId: endpoints?.sourceEpicId ?? null,
|
|
1354
|
+
targetEpicId: endpoints?.targetEpicId ?? null,
|
|
1355
|
+
createdBy: row.created_by,
|
|
1356
|
+
createdByAgentId: row.created_by_agent_id,
|
|
1357
|
+
createdAt: row.created_at,
|
|
1358
|
+
updatedAt: row.updated_at,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
mapRelationCandidate(row) {
|
|
1362
|
+
return {
|
|
1363
|
+
id: row.id,
|
|
1364
|
+
projectId: row.project_id,
|
|
1365
|
+
projectName: row.project_name,
|
|
1366
|
+
title: row.title,
|
|
1367
|
+
statusId: row.status_id,
|
|
1368
|
+
statusLabel: row.status_label,
|
|
1369
|
+
statusColor: row.status_color,
|
|
1370
|
+
statusMcpHidden: Boolean(row.status_mcp_hidden),
|
|
1371
|
+
parentId: row.parent_id,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
normalizeRelationListLimit(limit, label) {
|
|
1375
|
+
if (limit === undefined)
|
|
1376
|
+
return 50;
|
|
1377
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_RELATION_LIST_LIMIT) {
|
|
1378
|
+
throw new error_types_1.ValidationError(`${label} limit must be between 1 and ${MAX_RELATION_LIST_LIMIT}.`);
|
|
1379
|
+
}
|
|
1380
|
+
return limit;
|
|
1381
|
+
}
|
|
1382
|
+
normalizeRelationListOffset(offset, label) {
|
|
1383
|
+
if (offset === undefined)
|
|
1384
|
+
return 0;
|
|
1385
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
1386
|
+
throw new error_types_1.ValidationError(`${label} offset must be a non-negative integer.`);
|
|
1387
|
+
}
|
|
1388
|
+
return offset;
|
|
1389
|
+
}
|
|
698
1390
|
ensureValidEpicParentSync(projectId, parentId, childId) {
|
|
699
1391
|
if (!parentId) {
|
|
700
1392
|
return;
|
|
@@ -719,6 +1411,18 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
|
|
|
719
1411
|
});
|
|
720
1412
|
}
|
|
721
1413
|
if (childId) {
|
|
1414
|
+
const existingRelation = this.rawClient
|
|
1415
|
+
.prepare(`SELECT 1 FROM epic_relations
|
|
1416
|
+
WHERE (left_epic_id = ? AND right_epic_id = ?)
|
|
1417
|
+
OR (left_epic_id = ? AND right_epic_id = ?)
|
|
1418
|
+
LIMIT 1`)
|
|
1419
|
+
.get(childId, parentId, parentId, childId);
|
|
1420
|
+
if (existingRelation) {
|
|
1421
|
+
throw new error_types_1.ValidationError('A parent and direct sub-Epic cannot have a relation.', {
|
|
1422
|
+
epicId: childId,
|
|
1423
|
+
parentId,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
722
1426
|
const descendants = this.db
|
|
723
1427
|
.select({ id: schema_1.epics.id })
|
|
724
1428
|
.from(schema_1.epics)
|