obsidian-accomplishments-mcp 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -182
- package/dist/index.js +207 -38
- package/dist/index.js.map +1 -1
- package/dist/integration.test.d.ts +8 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +979 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/models/types.d.ts +1 -2
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/models/v2-types.d.ts +460 -0
- package/dist/models/v2-types.d.ts.map +1 -0
- package/dist/models/v2-types.js +137 -0
- package/dist/models/v2-types.js.map +1 -0
- package/dist/models/v2-types.test.d.ts +5 -0
- package/dist/models/v2-types.test.d.ts.map +1 -0
- package/dist/models/v2-types.test.js +133 -0
- package/dist/models/v2-types.test.js.map +1 -0
- package/dist/parsers/canvas-parser.d.ts +1 -1
- package/dist/parsers/canvas-parser.d.ts.map +1 -1
- package/dist/parsers/canvas-parser.js +1 -1
- package/dist/parsers/canvas-parser.js.map +1 -1
- package/dist/parsers/markdown-parser.js +9 -9
- package/dist/parsers/markdown-parser.js.map +1 -1
- package/dist/services/v2/archive-manager.d.ts +96 -0
- package/dist/services/v2/archive-manager.d.ts.map +1 -0
- package/dist/services/v2/archive-manager.js +281 -0
- package/dist/services/v2/archive-manager.js.map +1 -0
- package/dist/services/v2/canvas-manager.d.ts +155 -0
- package/dist/services/v2/canvas-manager.d.ts.map +1 -0
- package/dist/services/v2/canvas-manager.js +540 -0
- package/dist/services/v2/canvas-manager.js.map +1 -0
- package/dist/services/v2/canvas-manager.test.d.ts +5 -0
- package/dist/services/v2/canvas-manager.test.d.ts.map +1 -0
- package/dist/services/v2/canvas-manager.test.js +327 -0
- package/dist/services/v2/canvas-manager.test.js.map +1 -0
- package/dist/services/v2/cascade-manager.d.ts +54 -0
- package/dist/services/v2/cascade-manager.d.ts.map +1 -0
- package/dist/services/v2/cascade-manager.js +220 -0
- package/dist/services/v2/cascade-manager.js.map +1 -0
- package/dist/services/v2/cycle-detector.d.ts +76 -0
- package/dist/services/v2/cycle-detector.d.ts.map +1 -0
- package/dist/services/v2/cycle-detector.js +183 -0
- package/dist/services/v2/cycle-detector.js.map +1 -0
- package/dist/services/v2/cycle-detector.test.d.ts +7 -0
- package/dist/services/v2/cycle-detector.test.d.ts.map +1 -0
- package/dist/services/v2/cycle-detector.test.js +125 -0
- package/dist/services/v2/cycle-detector.test.js.map +1 -0
- package/dist/services/v2/entity-parser.d.ts +54 -0
- package/dist/services/v2/entity-parser.d.ts.map +1 -0
- package/dist/services/v2/entity-parser.js +418 -0
- package/dist/services/v2/entity-parser.js.map +1 -0
- package/dist/services/v2/entity-parser.test.d.ts +5 -0
- package/dist/services/v2/entity-parser.test.d.ts.map +1 -0
- package/dist/services/v2/entity-parser.test.js +637 -0
- package/dist/services/v2/entity-parser.test.js.map +1 -0
- package/dist/services/v2/entity-serializer.d.ts +94 -0
- package/dist/services/v2/entity-serializer.d.ts.map +1 -0
- package/dist/services/v2/entity-serializer.js +583 -0
- package/dist/services/v2/entity-serializer.js.map +1 -0
- package/dist/services/v2/entity-serializer.test.d.ts +5 -0
- package/dist/services/v2/entity-serializer.test.d.ts.map +1 -0
- package/dist/services/v2/entity-serializer.test.js +241 -0
- package/dist/services/v2/entity-serializer.test.js.map +1 -0
- package/dist/services/v2/entity-validator.d.ts +65 -0
- package/dist/services/v2/entity-validator.d.ts.map +1 -0
- package/dist/services/v2/entity-validator.js +573 -0
- package/dist/services/v2/entity-validator.js.map +1 -0
- package/dist/services/v2/entity-validator.test.d.ts +5 -0
- package/dist/services/v2/entity-validator.test.d.ts.map +1 -0
- package/dist/services/v2/entity-validator.test.js +519 -0
- package/dist/services/v2/entity-validator.test.js.map +1 -0
- package/dist/services/v2/file-manager.d.ts +73 -0
- package/dist/services/v2/file-manager.d.ts.map +1 -0
- package/dist/services/v2/file-manager.js +310 -0
- package/dist/services/v2/file-manager.js.map +1 -0
- package/dist/services/v2/file-manager.test.d.ts +5 -0
- package/dist/services/v2/file-manager.test.d.ts.map +1 -0
- package/dist/services/v2/file-manager.test.js +339 -0
- package/dist/services/v2/file-manager.test.js.map +1 -0
- package/dist/services/v2/index-manager.d.ts +68 -0
- package/dist/services/v2/index-manager.d.ts.map +1 -0
- package/dist/services/v2/index-manager.js +228 -0
- package/dist/services/v2/index-manager.js.map +1 -0
- package/dist/services/v2/index-manager.test.d.ts +5 -0
- package/dist/services/v2/index-manager.test.d.ts.map +1 -0
- package/dist/services/v2/index-manager.test.js +386 -0
- package/dist/services/v2/index-manager.test.js.map +1 -0
- package/dist/services/v2/index-service.d.ts +82 -0
- package/dist/services/v2/index-service.d.ts.map +1 -0
- package/dist/services/v2/index-service.js +274 -0
- package/dist/services/v2/index-service.js.map +1 -0
- package/dist/services/v2/index-service.test.d.ts +5 -0
- package/dist/services/v2/index-service.test.d.ts.map +1 -0
- package/dist/services/v2/index-service.test.js +117 -0
- package/dist/services/v2/index-service.test.js.map +1 -0
- package/dist/services/v2/lifecycle-manager.d.ts +59 -0
- package/dist/services/v2/lifecycle-manager.d.ts.map +1 -0
- package/dist/services/v2/lifecycle-manager.js +310 -0
- package/dist/services/v2/lifecycle-manager.js.map +1 -0
- package/dist/services/v2/lifecycle-manager.test.d.ts +5 -0
- package/dist/services/v2/lifecycle-manager.test.d.ts.map +1 -0
- package/dist/services/v2/lifecycle-manager.test.js +141 -0
- package/dist/services/v2/lifecycle-manager.test.js.map +1 -0
- package/dist/services/v2/path-resolver.d.ts +64 -0
- package/dist/services/v2/path-resolver.d.ts.map +1 -0
- package/dist/services/v2/path-resolver.js +174 -0
- package/dist/services/v2/path-resolver.js.map +1 -0
- package/dist/services/v2/progress-computer.d.ts +46 -0
- package/dist/services/v2/progress-computer.d.ts.map +1 -0
- package/dist/services/v2/progress-computer.js +200 -0
- package/dist/services/v2/progress-computer.js.map +1 -0
- package/dist/services/v2/search-service.d.ts +68 -0
- package/dist/services/v2/search-service.d.ts.map +1 -0
- package/dist/services/v2/search-service.js +194 -0
- package/dist/services/v2/search-service.js.map +1 -0
- package/dist/services/v2/transitive-dependency-remover.d.ts +54 -0
- package/dist/services/v2/transitive-dependency-remover.d.ts.map +1 -0
- package/dist/services/v2/transitive-dependency-remover.js +156 -0
- package/dist/services/v2/transitive-dependency-remover.js.map +1 -0
- package/dist/services/v2/transitive-dependency-remover.test.d.ts +7 -0
- package/dist/services/v2/transitive-dependency-remover.test.d.ts.map +1 -0
- package/dist/services/v2/transitive-dependency-remover.test.js +119 -0
- package/dist/services/v2/transitive-dependency-remover.test.js.map +1 -0
- package/dist/services/v2/v2-runtime.d.ts +374 -0
- package/dist/services/v2/v2-runtime.d.ts.map +1 -0
- package/dist/services/v2/v2-runtime.js +1908 -0
- package/dist/services/v2/v2-runtime.js.map +1 -0
- package/dist/services/v2/v2-runtime.test.d.ts +5 -0
- package/dist/services/v2/v2-runtime.test.d.ts.map +1 -0
- package/dist/services/v2/v2-runtime.test.js +658 -0
- package/dist/services/v2/v2-runtime.test.js.map +1 -0
- package/dist/services/v2/workstream-normalizer.d.ts +59 -0
- package/dist/services/v2/workstream-normalizer.d.ts.map +1 -0
- package/dist/services/v2/workstream-normalizer.js +137 -0
- package/dist/services/v2/workstream-normalizer.js.map +1 -0
- package/dist/services/v2/workstream-normalizer.test.d.ts +7 -0
- package/dist/services/v2/workstream-normalizer.test.d.ts.map +1 -0
- package/dist/services/v2/workstream-normalizer.test.js +130 -0
- package/dist/services/v2/workstream-normalizer.test.js.map +1 -0
- package/dist/test-runner.d.ts +4 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +44 -249
- package/dist/test-runner.js.map +1 -1
- package/dist/tools/batch-operations-tools.d.ts +54 -0
- package/dist/tools/batch-operations-tools.d.ts.map +1 -0
- package/dist/tools/batch-operations-tools.js +370 -0
- package/dist/tools/batch-operations-tools.js.map +1 -0
- package/dist/tools/decision-document-tools.d.ts +78 -0
- package/dist/tools/decision-document-tools.d.ts.map +1 -0
- package/dist/tools/decision-document-tools.js +260 -0
- package/dist/tools/decision-document-tools.js.map +1 -0
- package/dist/tools/entity-management-tools.d.ts +79 -0
- package/dist/tools/entity-management-tools.d.ts.map +1 -0
- package/dist/tools/entity-management-tools.js +851 -0
- package/dist/tools/entity-management-tools.js.map +1 -0
- package/dist/tools/entity-management-tools.test.d.ts +5 -0
- package/dist/tools/entity-management-tools.test.d.ts.map +1 -0
- package/dist/tools/entity-management-tools.test.js +530 -0
- package/dist/tools/entity-management-tools.test.js.map +1 -0
- package/dist/tools/index.d.ts +15 -271
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +510 -47
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/index.test.d.ts +8 -0
- package/dist/tools/index.test.d.ts.map +1 -0
- package/dist/tools/index.test.js +429 -0
- package/dist/tools/index.test.js.map +1 -0
- package/dist/tools/project-understanding-tools.d.ts +75 -0
- package/dist/tools/project-understanding-tools.d.ts.map +1 -0
- package/dist/tools/project-understanding-tools.js +751 -0
- package/dist/tools/project-understanding-tools.js.map +1 -0
- package/dist/tools/search-navigation-tools.d.ts +77 -0
- package/dist/tools/search-navigation-tools.d.ts.map +1 -0
- package/dist/tools/search-navigation-tools.js +379 -0
- package/dist/tools/search-navigation-tools.js.map +1 -0
- package/dist/tools/tool-types.d.ts +703 -0
- package/dist/tools/tool-types.d.ts.map +1 -0
- package/dist/tools/tool-types.js +7 -0
- package/dist/tools/tool-types.js.map +1 -0
- package/dist/utils/config.d.ts +0 -4
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +2 -19
- package/dist/utils/config.js.map +1 -1
- package/package.json +16 -1
- package/dist/services/accomplishment-service.d.ts +0 -33
- package/dist/services/accomplishment-service.d.ts.map +0 -1
- package/dist/services/accomplishment-service.js +0 -293
- package/dist/services/accomplishment-service.js.map +0 -1
- package/dist/services/canvas-service.d.ts +0 -96
- package/dist/services/canvas-service.d.ts.map +0 -1
- package/dist/services/canvas-service.js +0 -231
- package/dist/services/canvas-service.js.map +0 -1
- package/dist/services/context-doc-service.d.ts +0 -70
- package/dist/services/context-doc-service.d.ts.map +0 -1
- package/dist/services/context-doc-service.js +0 -229
- package/dist/services/context-doc-service.js.map +0 -1
- package/dist/services/dependency-service.d.ts +0 -22
- package/dist/services/dependency-service.d.ts.map +0 -1
- package/dist/services/dependency-service.js +0 -99
- package/dist/services/dependency-service.js.map +0 -1
- package/dist/services/status-indicator-service.d.ts +0 -40
- package/dist/services/status-indicator-service.d.ts.map +0 -1
- package/dist/services/status-indicator-service.js +0 -173
- package/dist/services/status-indicator-service.js.map +0 -1
- package/dist/services/task-service.d.ts +0 -32
- package/dist/services/task-service.d.ts.map +0 -1
- package/dist/services/task-service.js +0 -152
- package/dist/services/task-service.js.map +0 -1
- package/dist/test-real-vault.d.ts +0 -6
- package/dist/test-real-vault.d.ts.map +0 -1
- package/dist/test-real-vault.js +0 -30
- package/dist/test-real-vault.js.map +0 -1
- package/dist/tools/batch-operations.d.ts +0 -246
- package/dist/tools/batch-operations.d.ts.map +0 -1
- package/dist/tools/batch-operations.js +0 -235
- package/dist/tools/batch-operations.js.map +0 -1
- package/dist/tools/get-accomplishment.d.ts +0 -26
- package/dist/tools/get-accomplishment.d.ts.map +0 -1
- package/dist/tools/get-accomplishment.js +0 -53
- package/dist/tools/get-accomplishment.js.map +0 -1
- package/dist/tools/get-accomplishments-graph.d.ts +0 -26
- package/dist/tools/get-accomplishments-graph.d.ts.map +0 -1
- package/dist/tools/get-accomplishments-graph.js +0 -137
- package/dist/tools/get-accomplishments-graph.js.map +0 -1
- package/dist/tools/get-blocked-items.d.ts +0 -15
- package/dist/tools/get-blocked-items.d.ts.map +0 -1
- package/dist/tools/get-blocked-items.js +0 -73
- package/dist/tools/get-blocked-items.js.map +0 -1
- package/dist/tools/get-current-work.d.ts +0 -15
- package/dist/tools/get-current-work.d.ts.map +0 -1
- package/dist/tools/get-current-work.js +0 -68
- package/dist/tools/get-current-work.js.map +0 -1
- package/dist/tools/get-project-status.d.ts +0 -26
- package/dist/tools/get-project-status.d.ts.map +0 -1
- package/dist/tools/get-project-status.js +0 -98
- package/dist/tools/get-project-status.js.map +0 -1
- package/dist/tools/get-ready-to-start.d.ts +0 -15
- package/dist/tools/get-ready-to-start.d.ts.map +0 -1
- package/dist/tools/get-ready-to-start.js +0 -47
- package/dist/tools/get-ready-to-start.js.map +0 -1
- package/dist/tools/list-accomplishments.d.ts +0 -34
- package/dist/tools/list-accomplishments.d.ts.map +0 -1
- package/dist/tools/list-accomplishments.js +0 -34
- package/dist/tools/list-accomplishments.js.map +0 -1
- package/dist/tools/manage-accomplishment.d.ts +0 -147
- package/dist/tools/manage-accomplishment.d.ts.map +0 -1
- package/dist/tools/manage-accomplishment.js +0 -153
- package/dist/tools/manage-accomplishment.js.map +0 -1
- package/dist/tools/manage-dependency.d.ts +0 -41
- package/dist/tools/manage-dependency.d.ts.map +0 -1
- package/dist/tools/manage-dependency.js +0 -66
- package/dist/tools/manage-dependency.js.map +0 -1
- package/dist/tools/manage-task.d.ts +0 -119
- package/dist/tools/manage-task.d.ts.map +0 -1
- package/dist/tools/manage-task.js +0 -126
- package/dist/tools/manage-task.js.map +0 -1
- package/dist/tools/reconcile-canvas.d.ts +0 -33
- package/dist/tools/reconcile-canvas.d.ts.map +0 -1
- package/dist/tools/reconcile-canvas.js +0 -41
- package/dist/tools/reconcile-canvas.js.map +0 -1
- package/dist/tools/set-work-focus.d.ts +0 -48
- package/dist/tools/set-work-focus.d.ts.map +0 -1
- package/dist/tools/set-work-focus.js +0 -78
- package/dist/tools/set-work-focus.js.map +0 -1
- package/dist/tools/sync-dependencies.d.ts +0 -33
- package/dist/tools/sync-dependencies.d.ts.map +0 -1
- package/dist/tools/sync-dependencies.js +0 -144
- package/dist/tools/sync-dependencies.js.map +0 -1
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 Entity Management Tools
|
|
3
|
+
*
|
|
4
|
+
* MCP tools for creating, updating, archiving, and restoring entities.
|
|
5
|
+
*/
|
|
6
|
+
import { generateCssClasses } from '../services/v2/entity-serializer.js';
|
|
7
|
+
import { workstreamNormalizer } from '../services/v2/workstream-normalizer.js';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Relationship Validation Constants
|
|
10
|
+
// =============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Valid target types for Decision.affects field.
|
|
13
|
+
* Decisions can affect: documents, stories, tasks (NOT milestones)
|
|
14
|
+
*/
|
|
15
|
+
const DECISION_AFFECTS_VALID_TYPES = ['document', 'story', 'task'];
|
|
16
|
+
/**
|
|
17
|
+
* Valid target types for Document.implemented_by field.
|
|
18
|
+
* Documents can be implemented by: stories, tasks (NOT milestones)
|
|
19
|
+
*/
|
|
20
|
+
const DOCUMENT_IMPLEMENTED_BY_VALID_TYPES = ['story', 'task'];
|
|
21
|
+
/**
|
|
22
|
+
* Valid target types for depends_on fields by entity type.
|
|
23
|
+
*/
|
|
24
|
+
const DEPENDS_ON_VALID_TYPES = {
|
|
25
|
+
milestone: ['milestone', 'decision'],
|
|
26
|
+
story: ['story', 'decision', 'document'],
|
|
27
|
+
task: ['task', 'decision'],
|
|
28
|
+
decision: ['decision'],
|
|
29
|
+
document: ['document', 'decision'],
|
|
30
|
+
feature: ['feature', 'decision'],
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Validate entity relationships before creation/update.
|
|
34
|
+
* Checks that:
|
|
35
|
+
* 1. All referenced entity IDs exist (or are in the batch being created)
|
|
36
|
+
* 2. Relationship types are valid (e.g., decisions can't enable milestones)
|
|
37
|
+
*
|
|
38
|
+
* @param type - The entity type being created/updated
|
|
39
|
+
* @param data - The entity data containing relationships
|
|
40
|
+
* @param deps - Dependencies for entity lookup
|
|
41
|
+
* @param batchIds - Optional map of IDs being created in the same batch (id -> type)
|
|
42
|
+
*/
|
|
43
|
+
export function validateRelationships(type, data, deps, batchIds) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
// Helper to check if an ID exists (in cache or batch)
|
|
46
|
+
const idExists = (id) => {
|
|
47
|
+
return deps.entityExists(id) || (batchIds?.has(id) ?? false);
|
|
48
|
+
};
|
|
49
|
+
// Helper to get entity type (from cache or batch)
|
|
50
|
+
const getType = (id) => {
|
|
51
|
+
const cachedType = deps.getEntityType(id);
|
|
52
|
+
if (cachedType)
|
|
53
|
+
return cachedType;
|
|
54
|
+
return batchIds?.get(id) ?? null;
|
|
55
|
+
};
|
|
56
|
+
// Validate parent reference
|
|
57
|
+
const parent = data.parent;
|
|
58
|
+
if (parent) {
|
|
59
|
+
if (!idExists(parent)) {
|
|
60
|
+
errors.push({
|
|
61
|
+
field: 'parent',
|
|
62
|
+
message: `Parent entity '${parent}' does not exist`,
|
|
63
|
+
invalidId: parent,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Validate parent type
|
|
68
|
+
const parentType = getType(parent);
|
|
69
|
+
if (type === 'story' && parentType && parentType !== 'milestone') {
|
|
70
|
+
errors.push({
|
|
71
|
+
field: 'parent',
|
|
72
|
+
message: `Story parent must be a milestone, got '${parentType}'`,
|
|
73
|
+
invalidId: parent,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (type === 'task' && parentType && parentType !== 'story') {
|
|
77
|
+
errors.push({
|
|
78
|
+
field: 'parent',
|
|
79
|
+
message: `Task parent must be a story, got '${parentType}'`,
|
|
80
|
+
invalidId: parent,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Validate depends_on references
|
|
86
|
+
const dependsOn = data.depends_on;
|
|
87
|
+
if (dependsOn && dependsOn.length > 0) {
|
|
88
|
+
const validTypes = DEPENDS_ON_VALID_TYPES[type];
|
|
89
|
+
for (const depId of dependsOn) {
|
|
90
|
+
if (!idExists(depId)) {
|
|
91
|
+
errors.push({
|
|
92
|
+
field: 'depends_on',
|
|
93
|
+
message: `Dependency '${depId}' does not exist`,
|
|
94
|
+
invalidId: depId,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const depType = getType(depId);
|
|
99
|
+
if (depType && !validTypes.includes(depType)) {
|
|
100
|
+
errors.push({
|
|
101
|
+
field: 'depends_on',
|
|
102
|
+
message: `${type} cannot depend on ${depType} '${depId}'. Valid types: ${validTypes.join(', ')}`,
|
|
103
|
+
invalidId: depId,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Validate implements references (for stories and milestones)
|
|
110
|
+
const implementsField = data.implements;
|
|
111
|
+
if (implementsField && implementsField.length > 0) {
|
|
112
|
+
for (const implId of implementsField) {
|
|
113
|
+
if (!idExists(implId)) {
|
|
114
|
+
errors.push({
|
|
115
|
+
field: 'implements',
|
|
116
|
+
message: `Document '${implId}' does not exist`,
|
|
117
|
+
invalidId: implId,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const implType = getType(implId);
|
|
122
|
+
if (implType && implType !== 'document') {
|
|
123
|
+
errors.push({
|
|
124
|
+
field: 'implements',
|
|
125
|
+
message: `Can only implement documents, got ${implType} '${implId}'`,
|
|
126
|
+
invalidId: implId,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Validate affects references (for decisions)
|
|
133
|
+
if (type === 'decision') {
|
|
134
|
+
const affects = data.affects;
|
|
135
|
+
if (affects && affects.length > 0) {
|
|
136
|
+
for (const affectedId of affects) {
|
|
137
|
+
if (!idExists(affectedId)) {
|
|
138
|
+
errors.push({
|
|
139
|
+
field: 'affects',
|
|
140
|
+
message: `Entity '${affectedId}' does not exist`,
|
|
141
|
+
invalidId: affectedId,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const affectedType = getType(affectedId);
|
|
146
|
+
if (affectedType && !DECISION_AFFECTS_VALID_TYPES.includes(affectedType)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
field: 'affects',
|
|
149
|
+
message: `Decision cannot affect ${affectedType} '${affectedId}'. Valid types: ${DECISION_AFFECTS_VALID_TYPES.join(', ')}`,
|
|
150
|
+
invalidId: affectedId,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Validate implemented_by references (for documents)
|
|
158
|
+
if (type === 'document') {
|
|
159
|
+
const implementedBy = data.implemented_by;
|
|
160
|
+
if (implementedBy && implementedBy.length > 0) {
|
|
161
|
+
for (const implById of implementedBy) {
|
|
162
|
+
if (!idExists(implById)) {
|
|
163
|
+
errors.push({
|
|
164
|
+
field: 'implemented_by',
|
|
165
|
+
message: `Entity '${implById}' does not exist`,
|
|
166
|
+
invalidId: implById,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const implByType = getType(implById);
|
|
171
|
+
if (implByType && !DOCUMENT_IMPLEMENTED_BY_VALID_TYPES.includes(implByType)) {
|
|
172
|
+
errors.push({
|
|
173
|
+
field: 'implemented_by',
|
|
174
|
+
message: `Document cannot be implemented by ${implByType} '${implById}'. Valid types: ${DOCUMENT_IMPLEMENTED_BY_VALID_TYPES.join(', ')}`,
|
|
175
|
+
invalidId: implById,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Validate supersedes reference (for decisions)
|
|
183
|
+
if (type === 'decision') {
|
|
184
|
+
const supersedes = data.supersedes;
|
|
185
|
+
if (supersedes) {
|
|
186
|
+
if (!idExists(supersedes)) {
|
|
187
|
+
errors.push({
|
|
188
|
+
field: 'supersedes',
|
|
189
|
+
message: `Decision '${supersedes}' does not exist`,
|
|
190
|
+
invalidId: supersedes,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const supersedesType = getType(supersedes);
|
|
195
|
+
if (supersedesType && supersedesType !== 'decision') {
|
|
196
|
+
errors.push({
|
|
197
|
+
field: 'supersedes',
|
|
198
|
+
message: `Can only supersede decisions, got ${supersedesType} '${supersedes}'`,
|
|
199
|
+
invalidId: supersedes,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return errors;
|
|
206
|
+
}
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// Tool Implementations
|
|
209
|
+
// =============================================================================
|
|
210
|
+
/**
|
|
211
|
+
* Create a new entity with optional dependencies and relationships.
|
|
212
|
+
*/
|
|
213
|
+
export async function createEntity(input, deps) {
|
|
214
|
+
const { type, data, options } = input;
|
|
215
|
+
// Validate relationships before creating
|
|
216
|
+
const validationErrors = validateRelationships(type, data, deps);
|
|
217
|
+
if (validationErrors.length > 0) {
|
|
218
|
+
const errorMessages = validationErrors.map(e => `${e.field}: ${e.message}`).join('; ');
|
|
219
|
+
throw new Error(`Invalid relationships: ${errorMessages}`);
|
|
220
|
+
}
|
|
221
|
+
// Normalize workstream
|
|
222
|
+
const workstreamResult = workstreamNormalizer.normalize(data.workstream);
|
|
223
|
+
const normalizedWorkstream = workstreamResult.normalized;
|
|
224
|
+
// Generate new ID
|
|
225
|
+
const id = await deps.getNextId(type);
|
|
226
|
+
// Build base entity
|
|
227
|
+
const now = deps.getCurrentTimestamp();
|
|
228
|
+
const baseEntity = {
|
|
229
|
+
id,
|
|
230
|
+
type,
|
|
231
|
+
title: data.title,
|
|
232
|
+
workstream: normalizedWorkstream,
|
|
233
|
+
created_at: now,
|
|
234
|
+
updated_at: now,
|
|
235
|
+
archived: false,
|
|
236
|
+
canvas_source: options?.canvas_source,
|
|
237
|
+
};
|
|
238
|
+
// Build type-specific entity
|
|
239
|
+
let entity;
|
|
240
|
+
switch (type) {
|
|
241
|
+
case 'milestone':
|
|
242
|
+
entity = buildMilestone(baseEntity, data);
|
|
243
|
+
break;
|
|
244
|
+
case 'story':
|
|
245
|
+
entity = buildStory(baseEntity, data);
|
|
246
|
+
break;
|
|
247
|
+
case 'task':
|
|
248
|
+
entity = buildTask(baseEntity, data);
|
|
249
|
+
break;
|
|
250
|
+
case 'decision':
|
|
251
|
+
entity = buildDecision(baseEntity, data);
|
|
252
|
+
break;
|
|
253
|
+
case 'document':
|
|
254
|
+
entity = buildDocument(baseEntity, data);
|
|
255
|
+
break;
|
|
256
|
+
case 'feature':
|
|
257
|
+
entity = buildFeature(baseEntity, data);
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
throw new Error(`Unknown entity type: ${type}`);
|
|
261
|
+
}
|
|
262
|
+
// Write entity to file
|
|
263
|
+
await deps.writeEntity(entity);
|
|
264
|
+
// Add to canvas if requested
|
|
265
|
+
let canvasNodeAdded = false;
|
|
266
|
+
if (options?.add_to_canvas !== false && deps.addToCanvas && options?.canvas_source) {
|
|
267
|
+
canvasNodeAdded = await deps.addToCanvas(entity, options.canvas_source);
|
|
268
|
+
}
|
|
269
|
+
// Convert to full representation
|
|
270
|
+
const entityFull = await deps.toEntityFull(entity);
|
|
271
|
+
// Build result with optional normalization message
|
|
272
|
+
const result = {
|
|
273
|
+
id,
|
|
274
|
+
entity: entityFull,
|
|
275
|
+
dependencies_created: data.depends_on?.length ?? 0,
|
|
276
|
+
canvas_node_added: canvasNodeAdded,
|
|
277
|
+
};
|
|
278
|
+
// Add normalization message if workstream was normalized
|
|
279
|
+
if (workstreamResult.wasNormalized && workstreamResult.message) {
|
|
280
|
+
result.messages = result.messages || [];
|
|
281
|
+
result.messages.push(workstreamResult.message);
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// Entity Builders
|
|
287
|
+
// =============================================================================
|
|
288
|
+
function buildMilestone(base, data) {
|
|
289
|
+
const entity = {
|
|
290
|
+
...base,
|
|
291
|
+
type: 'milestone',
|
|
292
|
+
status: data.status || 'Not Started',
|
|
293
|
+
priority: data.priority,
|
|
294
|
+
target_date: data.target_date,
|
|
295
|
+
owner: data.owner,
|
|
296
|
+
depends_on: data.depends_on || [],
|
|
297
|
+
};
|
|
298
|
+
// Auto-generate cssclasses if not provided
|
|
299
|
+
entity.cssclasses = data.cssclasses || generateCssClasses(entity);
|
|
300
|
+
return entity;
|
|
301
|
+
}
|
|
302
|
+
function buildStory(base, data) {
|
|
303
|
+
const entity = {
|
|
304
|
+
...base,
|
|
305
|
+
type: 'story',
|
|
306
|
+
status: data.status || 'Not Started',
|
|
307
|
+
parent: data.parent,
|
|
308
|
+
priority: data.priority,
|
|
309
|
+
depends_on: data.depends_on || [],
|
|
310
|
+
implements: data.implements || [],
|
|
311
|
+
acceptance_criteria: data.acceptance_criteria || [],
|
|
312
|
+
tasks: data.tasks || [],
|
|
313
|
+
};
|
|
314
|
+
// Auto-generate cssclasses if not provided
|
|
315
|
+
entity.cssclasses = data.cssclasses || generateCssClasses(entity);
|
|
316
|
+
return entity;
|
|
317
|
+
}
|
|
318
|
+
function buildTask(base, data) {
|
|
319
|
+
const entity = {
|
|
320
|
+
...base,
|
|
321
|
+
type: 'task',
|
|
322
|
+
status: data.status || 'Not Started',
|
|
323
|
+
parent: data.parent,
|
|
324
|
+
goal: data.goal || '',
|
|
325
|
+
estimate_hrs: data.estimate_hrs,
|
|
326
|
+
actual_hrs: data.actual_hrs,
|
|
327
|
+
assignee: data.assignee,
|
|
328
|
+
description: data.description,
|
|
329
|
+
technical_notes: data.technical_notes,
|
|
330
|
+
notes: data.notes,
|
|
331
|
+
};
|
|
332
|
+
// Auto-generate cssclasses if not provided
|
|
333
|
+
entity.cssclasses = data.cssclasses || generateCssClasses(entity);
|
|
334
|
+
return entity;
|
|
335
|
+
}
|
|
336
|
+
function buildDecision(base, data) {
|
|
337
|
+
const entity = {
|
|
338
|
+
...base,
|
|
339
|
+
type: 'decision',
|
|
340
|
+
status: data.status || 'Pending',
|
|
341
|
+
decided_by: data.decided_by,
|
|
342
|
+
decided_on: data.decided_on,
|
|
343
|
+
supersedes: data.supersedes,
|
|
344
|
+
affects: data.affects || [],
|
|
345
|
+
depends_on: data.depends_on || [],
|
|
346
|
+
};
|
|
347
|
+
// Auto-generate cssclasses if not provided
|
|
348
|
+
entity.cssclasses = data.cssclasses || generateCssClasses(entity);
|
|
349
|
+
return entity;
|
|
350
|
+
}
|
|
351
|
+
function buildDocument(base, data) {
|
|
352
|
+
const entity = {
|
|
353
|
+
...base,
|
|
354
|
+
type: 'document',
|
|
355
|
+
doc_type: data.doc_type || 'spec',
|
|
356
|
+
status: data.status || 'Draft',
|
|
357
|
+
version: data.version,
|
|
358
|
+
owner: data.owner,
|
|
359
|
+
implementation_context: data.implementation_context,
|
|
360
|
+
implemented_by: data.implemented_by || [],
|
|
361
|
+
previous_version: data.previous_version || [],
|
|
362
|
+
content: data.content,
|
|
363
|
+
};
|
|
364
|
+
// Auto-generate cssclasses if not provided
|
|
365
|
+
entity.cssclasses = data.cssclasses || generateCssClasses(entity);
|
|
366
|
+
return entity;
|
|
367
|
+
}
|
|
368
|
+
function buildFeature(base, data) {
|
|
369
|
+
const entity = {
|
|
370
|
+
...base,
|
|
371
|
+
type: 'feature',
|
|
372
|
+
status: data.status || 'Planned',
|
|
373
|
+
user_story: data.user_story || '',
|
|
374
|
+
tier: data.tier || 'OSS',
|
|
375
|
+
phase: data.phase || 'MVP',
|
|
376
|
+
implemented_by: data.implemented_by || [],
|
|
377
|
+
documented_by: data.documented_by || [],
|
|
378
|
+
decided_by: data.decided_by || [],
|
|
379
|
+
test_refs: data.test_refs || [],
|
|
380
|
+
content: data.content,
|
|
381
|
+
};
|
|
382
|
+
// Auto-generate cssclasses if not provided
|
|
383
|
+
entity.cssclasses = data.cssclasses || generateCssClasses(entity);
|
|
384
|
+
return entity;
|
|
385
|
+
}
|
|
386
|
+
// =============================================================================
|
|
387
|
+
// Update Entity (Enhanced - consolidates status, archive, restore operations)
|
|
388
|
+
// =============================================================================
|
|
389
|
+
/**
|
|
390
|
+
* Update entity fields and/or modify relationships.
|
|
391
|
+
* Enhanced to support:
|
|
392
|
+
* - Status updates with validation and cascade (replaces update_entity_status)
|
|
393
|
+
* - Archive operations (replaces archive_entity, archive_milestone)
|
|
394
|
+
* - Restore operations (replaces restore_from_archive)
|
|
395
|
+
*/
|
|
396
|
+
export async function updateEntity(input, deps) {
|
|
397
|
+
const { id, data, add_dependencies, remove_dependencies, add_to, remove_from, status, status_note, cascade, archived, archive_options, restore_options, } = input;
|
|
398
|
+
// Get existing entity
|
|
399
|
+
const entity = await deps.getEntity(id);
|
|
400
|
+
if (!entity) {
|
|
401
|
+
throw new Error(`Entity not found: ${id}`);
|
|
402
|
+
}
|
|
403
|
+
// Initialize result
|
|
404
|
+
const result = {
|
|
405
|
+
id,
|
|
406
|
+
entity: null, // Will be set at the end
|
|
407
|
+
dependencies_added: 0,
|
|
408
|
+
dependencies_removed: 0,
|
|
409
|
+
};
|
|
410
|
+
// Handle archive operation (takes precedence)
|
|
411
|
+
if (archived === true) {
|
|
412
|
+
const archiveResult = await handleArchiveOperation(entity, archive_options, deps);
|
|
413
|
+
result.archive_result = archiveResult;
|
|
414
|
+
// After archiving, get the updated entity
|
|
415
|
+
const archivedEntity = await deps.getEntity(id);
|
|
416
|
+
if (archivedEntity) {
|
|
417
|
+
result.entity = await deps.toEntityFull(archivedEntity);
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
// Handle restore operation
|
|
422
|
+
if (archived === false && entity.archived) {
|
|
423
|
+
const restoreResult = await handleRestoreOperation(entity, restore_options, deps);
|
|
424
|
+
result.restore_result = restoreResult;
|
|
425
|
+
// After restoring, get the updated entity
|
|
426
|
+
const restoredEntity = await deps.getEntity(id);
|
|
427
|
+
if (restoredEntity) {
|
|
428
|
+
result.entity = await deps.toEntityFull(restoredEntity);
|
|
429
|
+
}
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
// Apply field updates
|
|
433
|
+
const updatedEntity = { ...entity };
|
|
434
|
+
let workstreamNormalizationMessage;
|
|
435
|
+
if (data) {
|
|
436
|
+
// Normalize workstream if being updated
|
|
437
|
+
if (data.workstream !== undefined) {
|
|
438
|
+
const workstreamResult = workstreamNormalizer.normalize(data.workstream);
|
|
439
|
+
data.workstream = workstreamResult.normalized;
|
|
440
|
+
if (workstreamResult.wasNormalized && workstreamResult.message) {
|
|
441
|
+
workstreamNormalizationMessage = workstreamResult.message;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
Object.assign(updatedEntity, data);
|
|
445
|
+
}
|
|
446
|
+
// Handle status update with validation and cascade
|
|
447
|
+
if (status && status !== entity.status) {
|
|
448
|
+
const statusResult = await handleStatusUpdate(entity, status, status_note, cascade, deps);
|
|
449
|
+
result.status_changed = statusResult;
|
|
450
|
+
updatedEntity.status = status;
|
|
451
|
+
}
|
|
452
|
+
// Handle dependency changes
|
|
453
|
+
if ('depends_on' in updatedEntity) {
|
|
454
|
+
const currentDeps = updatedEntity.depends_on || [];
|
|
455
|
+
if (add_dependencies) {
|
|
456
|
+
const newDeps = [...new Set([...currentDeps, ...add_dependencies])];
|
|
457
|
+
updatedEntity.depends_on = newDeps;
|
|
458
|
+
result.dependencies_added = newDeps.length - currentDeps.length;
|
|
459
|
+
}
|
|
460
|
+
if (remove_dependencies) {
|
|
461
|
+
const filtered = currentDeps.filter((d) => !remove_dependencies.includes(d));
|
|
462
|
+
updatedEntity.depends_on = filtered;
|
|
463
|
+
result.dependencies_removed = currentDeps.length - filtered.length;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Handle relationship additions (implements, affects)
|
|
467
|
+
if (add_to) {
|
|
468
|
+
if (add_to.implements && 'implements' in updatedEntity) {
|
|
469
|
+
const current = updatedEntity.implements || [];
|
|
470
|
+
updatedEntity.implements = [...new Set([...current, ...add_to.implements])];
|
|
471
|
+
}
|
|
472
|
+
if (add_to.affects && 'affects' in updatedEntity) {
|
|
473
|
+
const current = updatedEntity.affects || [];
|
|
474
|
+
updatedEntity.affects = [...new Set([...current, ...add_to.affects])];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Handle relationship removals
|
|
478
|
+
if (remove_from) {
|
|
479
|
+
if (remove_from.implements && 'implements' in updatedEntity) {
|
|
480
|
+
const current = updatedEntity.implements || [];
|
|
481
|
+
updatedEntity.implements = current.filter((i) => !remove_from.implements.includes(i));
|
|
482
|
+
}
|
|
483
|
+
if (remove_from.affects && 'affects' in updatedEntity) {
|
|
484
|
+
const current = updatedEntity.affects || [];
|
|
485
|
+
updatedEntity.affects = current.filter((e) => !remove_from.affects.includes(e));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Update timestamp
|
|
489
|
+
updatedEntity.updated_at = deps.getCurrentTimestamp();
|
|
490
|
+
// Compute field changes (before/after diff)
|
|
491
|
+
const changes = computeFieldChanges(entity, updatedEntity);
|
|
492
|
+
if (changes.length > 0) {
|
|
493
|
+
result.changes = changes;
|
|
494
|
+
}
|
|
495
|
+
// Write updated entity
|
|
496
|
+
await deps.writeEntity(updatedEntity);
|
|
497
|
+
// Convert to full representation
|
|
498
|
+
result.entity = await deps.toEntityFull(updatedEntity);
|
|
499
|
+
// Add normalization message if workstream was normalized
|
|
500
|
+
if (workstreamNormalizationMessage) {
|
|
501
|
+
result.messages = result.messages || [];
|
|
502
|
+
result.messages.push(workstreamNormalizationMessage);
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
// =============================================================================
|
|
507
|
+
// Helper: Compute Field Changes (before/after diff)
|
|
508
|
+
// =============================================================================
|
|
509
|
+
/**
|
|
510
|
+
* Compare two entity states and return a list of field changes.
|
|
511
|
+
* For array fields, also computes added/removed items.
|
|
512
|
+
*/
|
|
513
|
+
function computeFieldChanges(before, after) {
|
|
514
|
+
const changes = [];
|
|
515
|
+
// Get all keys from both objects
|
|
516
|
+
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
517
|
+
// Fields to skip (internal/computed)
|
|
518
|
+
const skipFields = new Set(['vault_path', 'created_at', 'updated_at']);
|
|
519
|
+
for (const key of allKeys) {
|
|
520
|
+
if (skipFields.has(key))
|
|
521
|
+
continue;
|
|
522
|
+
const beforeVal = before[key];
|
|
523
|
+
const afterVal = after[key];
|
|
524
|
+
// Check if values are different
|
|
525
|
+
const beforeStr = JSON.stringify(beforeVal);
|
|
526
|
+
const afterStr = JSON.stringify(afterVal);
|
|
527
|
+
if (beforeStr !== afterStr) {
|
|
528
|
+
const change = {
|
|
529
|
+
field: key,
|
|
530
|
+
before: beforeVal,
|
|
531
|
+
after: afterVal,
|
|
532
|
+
};
|
|
533
|
+
// For arrays, compute added/removed
|
|
534
|
+
if (Array.isArray(beforeVal) || Array.isArray(afterVal)) {
|
|
535
|
+
const beforeArr = Array.isArray(beforeVal) ? beforeVal : [];
|
|
536
|
+
const afterArr = Array.isArray(afterVal) ? afterVal : [];
|
|
537
|
+
const beforeSet = new Set(beforeArr.map(v => JSON.stringify(v)));
|
|
538
|
+
const afterSet = new Set(afterArr.map(v => JSON.stringify(v)));
|
|
539
|
+
const added = afterArr.filter(v => !beforeSet.has(JSON.stringify(v)));
|
|
540
|
+
const removed = beforeArr.filter(v => !afterSet.has(JSON.stringify(v)));
|
|
541
|
+
if (added.length > 0)
|
|
542
|
+
change.added = added;
|
|
543
|
+
if (removed.length > 0)
|
|
544
|
+
change.removed = removed;
|
|
545
|
+
}
|
|
546
|
+
changes.push(change);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return changes;
|
|
550
|
+
}
|
|
551
|
+
// =============================================================================
|
|
552
|
+
// Helper: Handle Status Update
|
|
553
|
+
// =============================================================================
|
|
554
|
+
async function handleStatusUpdate(entity, newStatus, note, cascade, deps) {
|
|
555
|
+
const oldStatus = entity.status;
|
|
556
|
+
// Validate transition
|
|
557
|
+
const validation = deps.validateStatusTransition(entity, newStatus);
|
|
558
|
+
if (!validation.valid) {
|
|
559
|
+
throw new Error(`Invalid status transition: ${validation.reason}`);
|
|
560
|
+
}
|
|
561
|
+
// Compute cascade effects if requested
|
|
562
|
+
let cascadedUpdates = [];
|
|
563
|
+
if (cascade) {
|
|
564
|
+
cascadedUpdates = await deps.computeCascadeEffects(entity, newStatus);
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
old_status: oldStatus,
|
|
568
|
+
new_status: newStatus,
|
|
569
|
+
cascaded_updates: cascadedUpdates,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// =============================================================================
|
|
573
|
+
// Helper: Handle Archive Operation
|
|
574
|
+
// =============================================================================
|
|
575
|
+
async function handleArchiveOperation(entity, options, deps) {
|
|
576
|
+
const { force, cascade: archiveCascade, archive_folder, remove_from_canvas, canvas_source } = options || {};
|
|
577
|
+
// For milestones with cascade, archive all children
|
|
578
|
+
if (entity.type === 'milestone' && archiveCascade) {
|
|
579
|
+
const archivedChildren = [];
|
|
580
|
+
// Compute archive path
|
|
581
|
+
const now = new Date();
|
|
582
|
+
const quarter = Math.ceil((now.getMonth() + 1) / 3);
|
|
583
|
+
const archivePath = archive_folder || `archive/${now.getFullYear()}-Q${quarter}`;
|
|
584
|
+
// Get all children (stories)
|
|
585
|
+
const stories = await deps.getChildren(entity.id);
|
|
586
|
+
for (const story of stories) {
|
|
587
|
+
// Get tasks for each story
|
|
588
|
+
const tasks = await deps.getChildren(story.id);
|
|
589
|
+
for (const task of tasks) {
|
|
590
|
+
await deps.moveToArchive(task.id, archivePath);
|
|
591
|
+
archivedChildren.push(task.id);
|
|
592
|
+
if (remove_from_canvas && deps.removeFromCanvas && canvas_source) {
|
|
593
|
+
await deps.removeFromCanvas(task.id, canvas_source);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
await deps.moveToArchive(story.id, archivePath);
|
|
597
|
+
archivedChildren.push(story.id);
|
|
598
|
+
if (remove_from_canvas && deps.removeFromCanvas && canvas_source) {
|
|
599
|
+
await deps.removeFromCanvas(story.id, canvas_source);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Archive the milestone itself
|
|
603
|
+
const finalPath = await deps.moveToArchive(entity.id, archivePath);
|
|
604
|
+
if (remove_from_canvas && deps.removeFromCanvas && canvas_source) {
|
|
605
|
+
await deps.removeFromCanvas(entity.id, canvas_source);
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
archived: true,
|
|
609
|
+
archive_path: finalPath,
|
|
610
|
+
archived_children: archivedChildren,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
// For non-cascade archive, check for children
|
|
614
|
+
if (!force) {
|
|
615
|
+
const children = await deps.getChildren(entity.id);
|
|
616
|
+
if (children.length > 0) {
|
|
617
|
+
throw new Error(`Entity has ${children.length} children. Use archive_options.force=true to archive anyway, or archive_options.cascade=true to archive children.`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Move to archive
|
|
621
|
+
const finalPath = await deps.moveToArchive(entity.id, archive_folder);
|
|
622
|
+
// Remove from canvas if requested
|
|
623
|
+
if (remove_from_canvas && deps.removeFromCanvas && canvas_source) {
|
|
624
|
+
await deps.removeFromCanvas(entity.id, canvas_source);
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
archived: true,
|
|
628
|
+
archive_path: finalPath,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
// =============================================================================
|
|
632
|
+
// Helper: Handle Restore Operation
|
|
633
|
+
// =============================================================================
|
|
634
|
+
async function handleRestoreOperation(entity, options, deps) {
|
|
635
|
+
const { restore_children, add_to_canvas, canvas_source } = options || {};
|
|
636
|
+
// Restore the entity
|
|
637
|
+
await deps.restoreFromArchive(entity.id);
|
|
638
|
+
// Add to canvas if requested
|
|
639
|
+
if (add_to_canvas && deps.addToCanvas && canvas_source) {
|
|
640
|
+
const restoredEntity = await deps.getEntity(entity.id);
|
|
641
|
+
if (restoredEntity) {
|
|
642
|
+
await deps.addToCanvas(restoredEntity, canvas_source);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Restore children if requested
|
|
646
|
+
const restoredChildren = [];
|
|
647
|
+
if (restore_children) {
|
|
648
|
+
const children = await deps.getChildren(entity.id);
|
|
649
|
+
for (const child of children) {
|
|
650
|
+
await deps.restoreFromArchive(child.id);
|
|
651
|
+
restoredChildren.push(child.id);
|
|
652
|
+
if (add_to_canvas && deps.addToCanvas && canvas_source) {
|
|
653
|
+
await deps.addToCanvas(child, canvas_source);
|
|
654
|
+
}
|
|
655
|
+
// Also restore grandchildren (tasks under stories)
|
|
656
|
+
const grandchildren = await deps.getChildren(child.id);
|
|
657
|
+
for (const grandchild of grandchildren) {
|
|
658
|
+
await deps.restoreFromArchive(grandchild.id);
|
|
659
|
+
restoredChildren.push(grandchild.id);
|
|
660
|
+
if (add_to_canvas && deps.addToCanvas && canvas_source) {
|
|
661
|
+
await deps.addToCanvas(grandchild, canvas_source);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
restored: true,
|
|
668
|
+
restored_children: restoredChildren.length > 0 ? restoredChildren : undefined,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
// =============================================================================
|
|
672
|
+
// Update Entity Status (DEPRECATED - use updateEntity with status field)
|
|
673
|
+
// =============================================================================
|
|
674
|
+
/**
|
|
675
|
+
* @deprecated Use updateEntity({ id, status, status_note, cascade }) instead.
|
|
676
|
+
* Dedicated status update with optional note and cascade.
|
|
677
|
+
*/
|
|
678
|
+
export async function updateEntityStatus(input, deps) {
|
|
679
|
+
const { id, status, note, cascade } = input;
|
|
680
|
+
// Get existing entity
|
|
681
|
+
const entity = await deps.getEntity(id);
|
|
682
|
+
if (!entity) {
|
|
683
|
+
throw new Error(`Entity not found: ${id}`);
|
|
684
|
+
}
|
|
685
|
+
const oldStatus = entity.status;
|
|
686
|
+
// Validate transition
|
|
687
|
+
const validation = deps.validateStatusTransition(entity, status);
|
|
688
|
+
if (!validation.valid) {
|
|
689
|
+
throw new Error(`Invalid status transition: ${validation.reason}`);
|
|
690
|
+
}
|
|
691
|
+
// Update entity - cast to Entity to handle status type variance
|
|
692
|
+
const updatedEntity = {
|
|
693
|
+
...entity,
|
|
694
|
+
status,
|
|
695
|
+
updated_at: deps.getCurrentTimestamp(),
|
|
696
|
+
};
|
|
697
|
+
// Add note to content if provided
|
|
698
|
+
if (note) {
|
|
699
|
+
// Note: In a real implementation, this would append to the entity's notes section
|
|
700
|
+
// For now, we just update the entity
|
|
701
|
+
}
|
|
702
|
+
// Write updated entity
|
|
703
|
+
await deps.writeEntity(updatedEntity);
|
|
704
|
+
// Compute cascade effects if requested
|
|
705
|
+
let cascadedUpdates = [];
|
|
706
|
+
if (cascade) {
|
|
707
|
+
cascadedUpdates = await deps.computeCascadeEffects(updatedEntity, status);
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
id,
|
|
711
|
+
old_status: oldStatus,
|
|
712
|
+
new_status: status,
|
|
713
|
+
cascaded_updates: cascadedUpdates,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
// =============================================================================
|
|
717
|
+
// Archive Entity (DEPRECATED - use updateEntity with archived: true)
|
|
718
|
+
// =============================================================================
|
|
719
|
+
/**
|
|
720
|
+
* @deprecated Use updateEntity({ id, archived: true, archive_options }) instead.
|
|
721
|
+
* Archive a single entity.
|
|
722
|
+
*/
|
|
723
|
+
export async function archiveEntity(input, deps) {
|
|
724
|
+
const { id, force, remove_from_canvas, canvas_source } = input;
|
|
725
|
+
// Get existing entity
|
|
726
|
+
const entity = await deps.getEntity(id);
|
|
727
|
+
if (!entity) {
|
|
728
|
+
throw new Error(`Entity not found: ${id}`);
|
|
729
|
+
}
|
|
730
|
+
// Check for children if not forcing
|
|
731
|
+
if (!force) {
|
|
732
|
+
const children = await deps.getChildren(id);
|
|
733
|
+
if (children.length > 0) {
|
|
734
|
+
throw new Error(`Entity has ${children.length} children. Use force=true to archive anyway.`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// Move to archive (runtime handles path computation based on config)
|
|
738
|
+
const finalPath = await deps.moveToArchive(id);
|
|
739
|
+
// Remove from canvas if requested
|
|
740
|
+
if (remove_from_canvas && deps.removeFromCanvas && canvas_source) {
|
|
741
|
+
await deps.removeFromCanvas(id, canvas_source);
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
id,
|
|
745
|
+
archived: true,
|
|
746
|
+
archive_path: finalPath,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
// =============================================================================
|
|
750
|
+
// Archive Milestone (DEPRECATED - use updateEntity with archived: true, archive_options.cascade: true)
|
|
751
|
+
// =============================================================================
|
|
752
|
+
/**
|
|
753
|
+
* @deprecated Use updateEntity({ id, archived: true, archive_options: { cascade: true } }) instead.
|
|
754
|
+
* Archive a milestone and all its children.
|
|
755
|
+
*/
|
|
756
|
+
export async function archiveMilestone(input, deps) {
|
|
757
|
+
const { milestone_id, archive_folder, remove_from_canvas, canvas_source } = input;
|
|
758
|
+
// Get milestone
|
|
759
|
+
const milestone = await deps.getEntity(milestone_id);
|
|
760
|
+
if (!milestone) {
|
|
761
|
+
throw new Error(`Milestone not found: ${milestone_id}`);
|
|
762
|
+
}
|
|
763
|
+
if (milestone.type !== 'milestone') {
|
|
764
|
+
throw new Error(`Entity ${milestone_id} is not a milestone`);
|
|
765
|
+
}
|
|
766
|
+
// Compute archive path
|
|
767
|
+
const now = new Date();
|
|
768
|
+
const quarter = Math.ceil((now.getMonth() + 1) / 3);
|
|
769
|
+
const archivePath = archive_folder || `archive/${now.getFullYear()}-Q${quarter}`;
|
|
770
|
+
// Collect all entities to archive
|
|
771
|
+
const archivedMilestones = [milestone_id];
|
|
772
|
+
const archivedStories = [];
|
|
773
|
+
const archivedTasks = [];
|
|
774
|
+
// Get all children (stories)
|
|
775
|
+
const stories = await deps.getChildren(milestone_id);
|
|
776
|
+
for (const story of stories) {
|
|
777
|
+
archivedStories.push(story.id);
|
|
778
|
+
// Get tasks for each story
|
|
779
|
+
const tasks = await deps.getChildren(story.id);
|
|
780
|
+
for (const task of tasks) {
|
|
781
|
+
archivedTasks.push(task.id);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Archive all entities and remove from canvas
|
|
785
|
+
const allIds = [...archivedTasks, ...archivedStories, ...archivedMilestones];
|
|
786
|
+
for (const id of allIds) {
|
|
787
|
+
await deps.moveToArchive(id, archivePath);
|
|
788
|
+
// Remove from canvas if requested
|
|
789
|
+
if (remove_from_canvas && deps.removeFromCanvas && canvas_source) {
|
|
790
|
+
await deps.removeFromCanvas(id, canvas_source);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
milestone_id,
|
|
795
|
+
archived_entities: {
|
|
796
|
+
milestones: archivedMilestones,
|
|
797
|
+
stories: archivedStories,
|
|
798
|
+
tasks: archivedTasks,
|
|
799
|
+
},
|
|
800
|
+
total_archived: archivedMilestones.length + archivedStories.length + archivedTasks.length,
|
|
801
|
+
archive_path: archivePath,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
// =============================================================================
|
|
805
|
+
// Restore From Archive (DEPRECATED - use updateEntity with archived: false)
|
|
806
|
+
// =============================================================================
|
|
807
|
+
/**
|
|
808
|
+
* @deprecated Use updateEntity({ id, archived: false, restore_options }) instead.
|
|
809
|
+
* Restore an archived entity.
|
|
810
|
+
*/
|
|
811
|
+
export async function restoreFromArchive(input, deps) {
|
|
812
|
+
const { id, restore_children, add_to_canvas, canvas_source } = input;
|
|
813
|
+
// Restore the entity
|
|
814
|
+
await deps.restoreFromArchive(id);
|
|
815
|
+
// Add to canvas if requested
|
|
816
|
+
if (add_to_canvas && deps.addToCanvas && canvas_source) {
|
|
817
|
+
const entity = await deps.getEntity(id);
|
|
818
|
+
if (entity) {
|
|
819
|
+
await deps.addToCanvas(entity, canvas_source);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// Restore children if requested
|
|
823
|
+
const restoredChildren = [];
|
|
824
|
+
if (restore_children) {
|
|
825
|
+
const children = await deps.getChildren(id);
|
|
826
|
+
for (const child of children) {
|
|
827
|
+
await deps.restoreFromArchive(child.id);
|
|
828
|
+
restoredChildren.push(child.id);
|
|
829
|
+
// Add child to canvas if requested
|
|
830
|
+
if (add_to_canvas && deps.addToCanvas && canvas_source) {
|
|
831
|
+
await deps.addToCanvas(child, canvas_source);
|
|
832
|
+
}
|
|
833
|
+
// Also restore grandchildren (tasks under stories)
|
|
834
|
+
const grandchildren = await deps.getChildren(child.id);
|
|
835
|
+
for (const grandchild of grandchildren) {
|
|
836
|
+
await deps.restoreFromArchive(grandchild.id);
|
|
837
|
+
restoredChildren.push(grandchild.id);
|
|
838
|
+
// Add grandchild to canvas if requested
|
|
839
|
+
if (add_to_canvas && deps.addToCanvas && canvas_source) {
|
|
840
|
+
await deps.addToCanvas(grandchild, canvas_source);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
id,
|
|
847
|
+
restored: true,
|
|
848
|
+
restored_children: restoredChildren,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
//# sourceMappingURL=entity-management-tools.js.map
|