@bpmsoftwaresolutions/ai-engine-client 1.1.86 → 1.1.88
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/package.json +1 -1
- package/src/client.js +136 -4463
- package/src/domains/implementation-artifacts.js +61 -0
- package/src/domains/implementation-checks.js +41 -0
- package/src/domains/implementation-items.js +82 -89
- package/src/domains/implementation-packets.js +173 -0
- package/src/domains/implementation-tasks.js +125 -6
- package/src/domains/portfolio.js +125 -1
- package/src/domains/project-chartering.js +18 -0
- package/src/domains/project-reports.js +14 -0
- package/src/domains/project-resume.js +18 -0
- package/src/domains/projects.js +348 -24
- package/src/domains/repo.js +124 -21
- package/src/domains/reports.js +16 -1
- package/src/domains/retrieval-management.js +19 -10
- package/src/domains/retrieval-wrapper.js +39 -3
- package/src/domains/roadmap-reports.js +6 -0
- package/src/domains/workflow-composition.js +3416 -7
- package/src/domains/workflows.js +99 -0
- package/src/index.js +1 -1
- package/src/utils/task-binding.js +31 -0
- package/src/utils/verified-mutations.js +128 -0
package/src/domains/portfolio.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cleanText, isPlainObject } from '../utils/text.js';
|
|
2
|
+
|
|
1
3
|
export function createPortfolioDomain(client) {
|
|
2
4
|
return {
|
|
3
5
|
getPortfolioStatus: (request) => client.getPortfolioStatus(request),
|
|
@@ -6,6 +8,128 @@ export function createPortfolioDomain(client) {
|
|
|
6
8
|
getPortfolioProject: (projectId) => client.getPortfolioProject(projectId),
|
|
7
9
|
getPortfolioReport: () => client.getPortfolioReport(),
|
|
8
10
|
getPortfolioBundle: () => client.getPortfolioBundle(),
|
|
9
|
-
getPortfolioClosureReadiness:
|
|
11
|
+
getPortfolioClosureReadiness: async ({
|
|
12
|
+
projectLimit = 25,
|
|
13
|
+
includeInactive = false,
|
|
14
|
+
includeLogaPortfolioProjection = false,
|
|
15
|
+
includeLogaRoadmapProjections = false,
|
|
16
|
+
} = {}) => {
|
|
17
|
+
const portfolioBundle = await client.getPortfolioBundle();
|
|
18
|
+
const projectListPayload = await client.listProjects({
|
|
19
|
+
limit: projectLimit,
|
|
20
|
+
includeInactive,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const bundleProjects = Array.isArray(portfolioBundle?.projects)
|
|
24
|
+
? portfolioBundle.projects.filter((project) => isPlainObject(project))
|
|
25
|
+
: [];
|
|
26
|
+
const listedProjects = Array.isArray(projectListPayload?.projects)
|
|
27
|
+
? projectListPayload.projects.filter((project) => isPlainObject(project))
|
|
28
|
+
: Array.isArray(projectListPayload)
|
|
29
|
+
? projectListPayload.filter((project) => isPlainObject(project))
|
|
30
|
+
: [];
|
|
31
|
+
const activeProjects = listedProjects.length > 0 ? listedProjects : bundleProjects;
|
|
32
|
+
const logaPortfolioProjection = includeLogaPortfolioProjection
|
|
33
|
+
? await client.getLogaProjectPortfolioProjection()
|
|
34
|
+
: null;
|
|
35
|
+
const logaRoadmapProjections = {};
|
|
36
|
+
|
|
37
|
+
const projectReadiness = [];
|
|
38
|
+
for (const project of activeProjects) {
|
|
39
|
+
const projectId = cleanText(project.project_id) || cleanText(project.projectId);
|
|
40
|
+
if (!projectId) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const roadmapSummaryPayload = await client.getProjectRoadmapSummary(projectId);
|
|
45
|
+
const roadmapSummary = isPlainObject(roadmapSummaryPayload?.summary) ? roadmapSummaryPayload.summary : {};
|
|
46
|
+
const activeItemPayload = await client.getProjectRoadmapActiveItem(projectId);
|
|
47
|
+
const activeItem = isPlainObject(activeItemPayload?.active_item)
|
|
48
|
+
? activeItemPayload.active_item
|
|
49
|
+
: isPlainObject(activeItemPayload)
|
|
50
|
+
? activeItemPayload
|
|
51
|
+
: {};
|
|
52
|
+
const openTasksPayload = await client.listProjectOpenTasks(projectId);
|
|
53
|
+
const openTasks = Array.isArray(openTasksPayload?.tasks)
|
|
54
|
+
? openTasksPayload.tasks.filter((task) => isPlainObject(task))
|
|
55
|
+
: Array.isArray(openTasksPayload)
|
|
56
|
+
? openTasksPayload.filter((task) => isPlainObject(task))
|
|
57
|
+
: [];
|
|
58
|
+
|
|
59
|
+
const activeItemStatus = cleanText(activeItem.item_status) || cleanText(activeItem.status);
|
|
60
|
+
const completionPercentage = Number(
|
|
61
|
+
roadmapSummary.completion_percentage
|
|
62
|
+
?? roadmapSummary.completion_pct
|
|
63
|
+
?? roadmapSummary.progress_percentage
|
|
64
|
+
?? 0,
|
|
65
|
+
);
|
|
66
|
+
const totalItems = Number(
|
|
67
|
+
roadmapSummary.total_items
|
|
68
|
+
?? roadmapSummary.item_count
|
|
69
|
+
?? roadmapSummary.total_count
|
|
70
|
+
?? 0,
|
|
71
|
+
);
|
|
72
|
+
const openItems = Number(
|
|
73
|
+
roadmapSummary.open_items
|
|
74
|
+
?? roadmapSummary.open_item_count
|
|
75
|
+
?? roadmapSummary.remaining_items
|
|
76
|
+
?? openTasks.length,
|
|
77
|
+
);
|
|
78
|
+
const terminalStatuses = new Set(['accepted', 'verified', 'done', 'completed', 'closed']);
|
|
79
|
+
const activeItemReady = !activeItemStatus || terminalStatuses.has(activeItemStatus.toLowerCase());
|
|
80
|
+
const projectReady = openItems === 0 && openTasks.length === 0 && activeItemReady && completionPercentage >= 100;
|
|
81
|
+
const blockingReason = projectReady
|
|
82
|
+
? null
|
|
83
|
+
: [
|
|
84
|
+
openTasks.length > 0 ? `${openTasks.length} open task(s) remain` : null,
|
|
85
|
+
openItems > 0 ? `${openItems} roadmap item(s) remain open` : null,
|
|
86
|
+
!activeItemReady ? `active item status is ${activeItemStatus || 'missing'}` : null,
|
|
87
|
+
completionPercentage < 100 ? `completion is ${completionPercentage.toFixed(1)}%` : null,
|
|
88
|
+
].filter(Boolean).join('; ') || 'portfolio closure readiness is not satisfied';
|
|
89
|
+
|
|
90
|
+
const roadmapCompletion = {
|
|
91
|
+
completion_percentage: completionPercentage,
|
|
92
|
+
total_items: totalItems,
|
|
93
|
+
open_items: openItems,
|
|
94
|
+
completed_items: Math.max(totalItems - openItems, 0),
|
|
95
|
+
};
|
|
96
|
+
const projectReadinessEntry = {
|
|
97
|
+
project: {
|
|
98
|
+
project_id: projectId,
|
|
99
|
+
project_name: cleanText(project.project_name) || cleanText(project.projectName) || null,
|
|
100
|
+
project_slug: cleanText(project.project_slug) || cleanText(project.projectSlug) || null,
|
|
101
|
+
process_status: cleanText(project.process_status) || cleanText(project.processStatus) || null,
|
|
102
|
+
charter_status: cleanText(project.charter_status) || cleanText(project.charterStatus) || null,
|
|
103
|
+
},
|
|
104
|
+
roadmap_completion: roadmapCompletion,
|
|
105
|
+
active_item: activeItem,
|
|
106
|
+
open_task_count: openTasks.length,
|
|
107
|
+
closure_ready: projectReady,
|
|
108
|
+
blocking_reason: blockingReason,
|
|
109
|
+
};
|
|
110
|
+
if (includeLogaRoadmapProjections) {
|
|
111
|
+
projectReadinessEntry.loga_roadmap_projection = await client.getLogaProjectRoadmapProjection(projectId);
|
|
112
|
+
logaRoadmapProjections[projectId] = projectReadinessEntry.loga_roadmap_projection;
|
|
113
|
+
}
|
|
114
|
+
projectReadiness.push(projectReadinessEntry);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const closureReady = projectReadiness.length === 0 || projectReadiness.every((project) => project.closure_ready === true);
|
|
118
|
+
const blockingProject = projectReadiness.find((project) => project.closure_ready !== true);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
portfolio_summary: isPlainObject(portfolioBundle?.summary) ? portfolioBundle.summary : {},
|
|
122
|
+
portfolio_bundle: portfolioBundle,
|
|
123
|
+
active_projects: projectReadiness,
|
|
124
|
+
closure_readiness: closureReady,
|
|
125
|
+
blocking_reason: closureReady ? null : `${cleanText(blockingProject?.project?.project_name) || cleanText(blockingProject?.project?.project_id) || 'project'}: ${blockingProject?.blocking_reason || 'closure readiness is not satisfied'}`,
|
|
126
|
+
blocking_project_id: closureReady ? null : cleanText(blockingProject?.project?.project_id),
|
|
127
|
+
blocking_project_name: closureReady ? null : cleanText(blockingProject?.project?.project_name),
|
|
128
|
+
project_count: projectReadiness.length,
|
|
129
|
+
open_task_count: projectReadiness.reduce((total, project) => total + Number(project.open_task_count || 0), 0),
|
|
130
|
+
loga_portfolio_projection: logaPortfolioProjection,
|
|
131
|
+
loga_roadmap_projections: includeLogaRoadmapProjections ? logaRoadmapProjections : null,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
10
134
|
};
|
|
11
135
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { isPlainObject } from '../utils/text.js';
|
|
2
|
+
import { normalizeTaskBindingPolicy } from '../utils/task-binding.js';
|
|
3
|
+
|
|
1
4
|
export function createProjectCharteringDomain(client) {
|
|
2
5
|
return {
|
|
3
6
|
createProjectDelivery: (body = {}) => client._request('/api/project-delivery', { method: 'POST', body }),
|
|
@@ -29,6 +32,21 @@ export function createProjectCharteringDomain(client) {
|
|
|
29
32
|
body,
|
|
30
33
|
});
|
|
31
34
|
},
|
|
35
|
+
routeImplementationItem: (implementationItemId, body = {}) => {
|
|
36
|
+
if (!implementationItemId) throw new Error('implementationItemId is required.');
|
|
37
|
+
const normalizedBody = isPlainObject(body)
|
|
38
|
+
? {
|
|
39
|
+
...body,
|
|
40
|
+
...(isPlainObject(body.task_binding)
|
|
41
|
+
? { task_binding: normalizeTaskBindingPolicy(body.task_binding) }
|
|
42
|
+
: {}),
|
|
43
|
+
}
|
|
44
|
+
: body;
|
|
45
|
+
return client._request(`/api/governed-implementation/items/${encodeURIComponent(implementationItemId)}/routing`, {
|
|
46
|
+
method: 'PATCH',
|
|
47
|
+
body: normalizedBody,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
32
50
|
createProjectCharter: (request = {}) => client._request('/api/projects/charter', {
|
|
33
51
|
method: 'POST',
|
|
34
52
|
body: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createProjectReportsDomain(client) {
|
|
2
|
+
return {
|
|
3
|
+
getProjectCharterReport: (projectId) => client._request(`/api/operator/projects/${projectId}/charter/report`),
|
|
4
|
+
createProjectMarkdownDownload: (projectId, request = {}) => client._request(`/api/operator/projects/${projectId}/markdown-report-downloads`, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
body: {
|
|
7
|
+
report_type: request.reportType,
|
|
8
|
+
include_markdown: request.includeMarkdown ?? false,
|
|
9
|
+
},
|
|
10
|
+
}),
|
|
11
|
+
downloadProjectMarkdownReport: (projectId, reportType) => client._requestText(`/api/operator/projects/${projectId}/markdown-reports/${reportType}/download`),
|
|
12
|
+
downloadProjectCharterReportMarkdown: (projectId) => client._requestText(`/api/operator/projects/${projectId}/markdown-reports/charter/download`),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function createProjectResumeDomain(client) {
|
|
2
|
+
return {
|
|
3
|
+
resumeProjectWork: (request = {}) => {
|
|
4
|
+
const normalizedProjectIdentifier = String(request.projectIdentifier || request.projectId || '').trim();
|
|
5
|
+
if (!normalizedProjectIdentifier) {
|
|
6
|
+
throw new Error('projectIdentifier is required.');
|
|
7
|
+
}
|
|
8
|
+
return client._request(`/api/operator/projects/${encodeURIComponent(normalizedProjectIdentifier)}/resume-context`, {
|
|
9
|
+
query: {
|
|
10
|
+
actor_mode: request.actorMode,
|
|
11
|
+
execution_intent: request.executionIntent,
|
|
12
|
+
require_claim: request.requireClaim,
|
|
13
|
+
workflow_run_limit: request.workflowRunLimit,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
package/src/domains/projects.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cleanList, cleanText, isPlainObject } from '../utils/text.js';
|
|
2
|
+
|
|
1
3
|
export function createProjectsDomain(client) {
|
|
2
4
|
return {
|
|
3
5
|
listProjects: (request) => client._request('/api/operator/projects', {
|
|
@@ -12,31 +14,12 @@ export function createProjectsDomain(client) {
|
|
|
12
14
|
listProjectWorkflowRuns: (projectId, request = {}) => client._request(`/api/operator/projects/${projectId}/workflow-runs`, {
|
|
13
15
|
query: { limit: request.limit ?? 25 },
|
|
14
16
|
}),
|
|
15
|
-
getProjectCharterReport: (projectId) => client._request(`/api/operator/projects/${projectId}/charter/report`),
|
|
16
|
-
createProjectMarkdownDownload: (projectId, request = {}) => client._request(`/api/operator/projects/${projectId}/markdown-report-downloads`, {
|
|
17
|
-
method: 'POST',
|
|
18
|
-
body: {
|
|
19
|
-
report_type: request.reportType,
|
|
20
|
-
include_markdown: request.includeMarkdown ?? false,
|
|
21
|
-
},
|
|
22
|
-
}),
|
|
23
|
-
downloadProjectMarkdownReport: (projectId, reportType) => client._requestText(`/api/operator/projects/${projectId}/markdown-reports/${reportType}/download`),
|
|
24
|
-
downloadProjectCharterReportMarkdown: (projectId) => client._requestText(`/api/operator/projects/${projectId}/markdown-reports/charter/download`),
|
|
25
17
|
getProjectBundle: (projectId) => client._request(`/api/operator/projects/${projectId}/bundle`),
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return client._request(`/api/operator/projects/${encodeURIComponent(normalizedProjectIdentifier)}/resume-context`, {
|
|
32
|
-
query: {
|
|
33
|
-
actor_mode: request.actorMode,
|
|
34
|
-
execution_intent: request.executionIntent,
|
|
35
|
-
require_claim: request.requireClaim,
|
|
36
|
-
workflow_run_limit: request.workflowRunLimit,
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
},
|
|
18
|
+
getProjectCharterReport: (projectId) => client.projectReports.getProjectCharterReport(projectId),
|
|
19
|
+
createProjectMarkdownDownload: (projectId, request = {}) => client.projectReports.createProjectMarkdownDownload(projectId, request),
|
|
20
|
+
downloadProjectMarkdownReport: (projectId, reportType) => client.projectReports.downloadProjectMarkdownReport(projectId, reportType),
|
|
21
|
+
downloadProjectCharterReportMarkdown: (projectId) => client.projectReports.downloadProjectCharterReportMarkdown(projectId),
|
|
22
|
+
resumeProjectWork: (request = {}) => client.projectResume.resumeProjectWork(request),
|
|
40
23
|
listProjectOpenTasks: (projectId) => client._request(`/api/operator/projects/${projectId}/open-tasks`),
|
|
41
24
|
getProjectPerformanceMetrics: (projectId, request = {}) => client._request(`/api/operator/projects/${projectId}/performance-metrics`, {
|
|
42
25
|
query: {
|
|
@@ -45,5 +28,346 @@ export function createProjectsDomain(client) {
|
|
|
45
28
|
since_utc: request.sinceUtc,
|
|
46
29
|
},
|
|
47
30
|
}),
|
|
31
|
+
closeProject: async (projectId, {
|
|
32
|
+
workflowId,
|
|
33
|
+
workflowRunIds,
|
|
34
|
+
reason,
|
|
35
|
+
operatorIdentity,
|
|
36
|
+
runTerminalStatus = 'failed',
|
|
37
|
+
} = {}) => {
|
|
38
|
+
const normalizedProjectId = cleanText(projectId);
|
|
39
|
+
if (!normalizedProjectId) {
|
|
40
|
+
throw new Error('projectId is required.');
|
|
41
|
+
}
|
|
42
|
+
const normalizedReason = cleanText(reason) || 'close active project';
|
|
43
|
+
const normalizedOperatorIdentity = cleanText(operatorIdentity) || client.actorId;
|
|
44
|
+
const resolvedRuns = Array.isArray(workflowRunIds) ? cleanList(workflowRunIds) : [];
|
|
45
|
+
let resolvedWorkflowId = cleanText(workflowId);
|
|
46
|
+
|
|
47
|
+
if (!resolvedWorkflowId || resolvedRuns.length === 0) {
|
|
48
|
+
const projectPayload = await client.getProject(normalizedProjectId);
|
|
49
|
+
const summary = isPlainObject(projectPayload?.summary) ? projectPayload.summary : {};
|
|
50
|
+
if (!resolvedWorkflowId) {
|
|
51
|
+
resolvedWorkflowId = cleanText(summary.workflow_id) || cleanText(summary.workflowId);
|
|
52
|
+
}
|
|
53
|
+
if (resolvedRuns.length === 0) {
|
|
54
|
+
const workflowRuns = await client.listProjectWorkflowRuns(normalizedProjectId, { limit: 100 });
|
|
55
|
+
const activeStatuses = new Set(['queued', 'running', 'blocked', 'open', 'active', 'ping-review']);
|
|
56
|
+
const activeRuns = Array.isArray(workflowRuns)
|
|
57
|
+
? workflowRuns.filter((run) => activeStatuses.has(String(run?.status || '').trim().toLowerCase()))
|
|
58
|
+
: [];
|
|
59
|
+
for (const run of activeRuns) {
|
|
60
|
+
const runId = cleanText(run.workflow_run_id || run.workflowRunId);
|
|
61
|
+
if (runId && !resolvedRuns.includes(runId)) {
|
|
62
|
+
resolvedRuns.push(runId);
|
|
63
|
+
}
|
|
64
|
+
if (!resolvedWorkflowId) {
|
|
65
|
+
resolvedWorkflowId = cleanText(run.workflow_id || run.workflowId) || resolvedWorkflowId;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (resolvedRuns.length > 0 && !resolvedWorkflowId) {
|
|
72
|
+
throw new Error('workflowId could not be resolved for project cleanup.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cleanupPayload = {
|
|
76
|
+
project_id: normalizedProjectId,
|
|
77
|
+
reason: normalizedReason,
|
|
78
|
+
operator_identity: normalizedOperatorIdentity,
|
|
79
|
+
run_terminal_status: cleanText(runTerminalStatus) || 'failed',
|
|
80
|
+
};
|
|
81
|
+
if (resolvedWorkflowId) {
|
|
82
|
+
cleanupPayload.workflow_id = resolvedWorkflowId;
|
|
83
|
+
}
|
|
84
|
+
if (resolvedRuns.length > 0) {
|
|
85
|
+
cleanupPayload.workflow_run_ids = resolvedRuns;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return client._request('/api/operator/cleanup', {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: cleanupPayload,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
closeActiveProject: (projectId, options = {}) => client.projects.closeProject(projectId, options),
|
|
94
|
+
closeRoadmapItemWorkflow: async ({
|
|
95
|
+
projectIdentifier,
|
|
96
|
+
projectId,
|
|
97
|
+
claimId,
|
|
98
|
+
claimName,
|
|
99
|
+
actorId,
|
|
100
|
+
actorMode = 'operator',
|
|
101
|
+
executionIntent = 'close roadmap item workflow',
|
|
102
|
+
workflowRunLimit = 5,
|
|
103
|
+
declaredScopeFiles,
|
|
104
|
+
allowedMutationSurfaces,
|
|
105
|
+
requiredArtifacts,
|
|
106
|
+
requiredAcceptanceCheckStatus = 'verified',
|
|
107
|
+
terminalItemStatus = 'accepted',
|
|
108
|
+
gateType = 'roadmap_closure',
|
|
109
|
+
gateDecision = 'pass',
|
|
110
|
+
gateRationale,
|
|
111
|
+
gateEvidenceRefs,
|
|
112
|
+
remediationActions = [],
|
|
113
|
+
closureEvidenceType = 'roadmap_closure',
|
|
114
|
+
closureEvidenceRef,
|
|
115
|
+
closureEvidenceTitle,
|
|
116
|
+
closeProjectIfNoRemainingOpenItems = true,
|
|
117
|
+
closeProjectReason,
|
|
118
|
+
} = {}) => {
|
|
119
|
+
const normalizedProjectReference = cleanText(projectIdentifier) || cleanText(projectId);
|
|
120
|
+
if (!normalizedProjectReference) {
|
|
121
|
+
throw new Error('projectIdentifier is required.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const normalizedActorId = cleanText(actorId) || client.actorId;
|
|
125
|
+
const continuation = await client.resumeProjectWork({
|
|
126
|
+
projectIdentifier: normalizedProjectReference,
|
|
127
|
+
projectId: normalizedProjectReference,
|
|
128
|
+
actorMode,
|
|
129
|
+
executionIntent,
|
|
130
|
+
requireClaim: false,
|
|
131
|
+
workflowRunLimit,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const projectSummary = isPlainObject(continuation?.project) ? continuation.project : {};
|
|
135
|
+
const resolvedProjectId = cleanText(projectSummary.project_id) || cleanText(projectSummary.projectId) || normalizedProjectReference;
|
|
136
|
+
const resolvedWorkflowId = cleanText(projectSummary.workflow_id) || cleanText(projectSummary.workflowId);
|
|
137
|
+
const resolvedWorkflowRunId = cleanText(projectSummary.workflow_run_id) || cleanText(projectSummary.workflowRunId) || null;
|
|
138
|
+
const activeItem = await client.getProjectRoadmapActiveItem(resolvedProjectId);
|
|
139
|
+
const normalizedActiveItem = isPlainObject(activeItem) ? activeItem : {};
|
|
140
|
+
const implementationItemId = cleanText(normalizedActiveItem.implementation_item_id) || cleanText(normalizedActiveItem.implementationItemId);
|
|
141
|
+
const itemKey = cleanText(normalizedActiveItem.item_key) || cleanText(normalizedActiveItem.itemKey) || implementationItemId;
|
|
142
|
+
const itemStatus = cleanText(normalizedActiveItem.item_status) || cleanText(normalizedActiveItem.status);
|
|
143
|
+
const packetId = cleanText(normalizedActiveItem.implementation_packet_id) || cleanText(normalizedActiveItem.implementationPacketId);
|
|
144
|
+
|
|
145
|
+
if (!implementationItemId || !resolvedWorkflowId || !packetId) {
|
|
146
|
+
throw new Error('project closure state is ambiguous.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let effectiveClaimId = cleanText(claimId);
|
|
150
|
+
let claimSource = 'provided';
|
|
151
|
+
if (effectiveClaimId) {
|
|
152
|
+
const claimStillValid = await client.claimIsValid(effectiveClaimId).catch(() => false);
|
|
153
|
+
if (!claimStillValid) {
|
|
154
|
+
effectiveClaimId = null;
|
|
155
|
+
claimSource = 'fresh';
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
claimSource = 'fresh';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let freshClaim = null;
|
|
162
|
+
if (!effectiveClaimId) {
|
|
163
|
+
const declaredScope = cleanList(declaredScopeFiles);
|
|
164
|
+
const surfaces = cleanList(allowedMutationSurfaces);
|
|
165
|
+
const fallbackDeclaredScope = declaredScope.length > 0 ? declaredScope : [
|
|
166
|
+
`${resolvedProjectId}`,
|
|
167
|
+
itemKey || implementationItemId,
|
|
168
|
+
].filter(Boolean);
|
|
169
|
+
const fallbackSurfaces = surfaces.length > 0 ? surfaces : [
|
|
170
|
+
'project_roadmap_task',
|
|
171
|
+
'implementation_acceptance_check',
|
|
172
|
+
'implementation_evidence',
|
|
173
|
+
'implementation_gate',
|
|
174
|
+
'project_close',
|
|
175
|
+
];
|
|
176
|
+
const closureTaskStatus = itemStatus && !new Set(['done', 'completed', 'complete', 'blocked', 'failed', 'rejected', 'cancelled']).has(itemStatus.toLowerCase())
|
|
177
|
+
? itemStatus
|
|
178
|
+
: 'in_progress';
|
|
179
|
+
const closureTaskBinding = {
|
|
180
|
+
task_id: implementationItemId || itemKey || resolvedProjectId,
|
|
181
|
+
roadmap_item_id: implementationItemId || itemKey || resolvedProjectId,
|
|
182
|
+
task_status: closureTaskStatus,
|
|
183
|
+
allowed_actions: ['claim_work_item'],
|
|
184
|
+
substrate_action: 'claim_work_item',
|
|
185
|
+
task_type: 'roadmap_closure',
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
freshClaim = await client.startClaimedWork({
|
|
189
|
+
claimName: cleanText(claimName) || `closeRoadmapItemWorkflow:${resolvedProjectId}:${itemKey || implementationItemId}`,
|
|
190
|
+
actorId: normalizedActorId,
|
|
191
|
+
intentId: 'code_mutation',
|
|
192
|
+
declaredScopeFiles: fallbackDeclaredScope,
|
|
193
|
+
allowedMutationSurfaces: fallbackSurfaces,
|
|
194
|
+
runtimeSessionId: resolvedWorkflowRunId || resolvedProjectId,
|
|
195
|
+
executionPurpose: cleanText(executionIntent) || `Close roadmap item ${itemKey || implementationItemId}`,
|
|
196
|
+
successCriteria: 'Active roadmap item closed with verified acceptance checks, artifacts, gate decision, and claim signoff.',
|
|
197
|
+
mutationAllowed: true,
|
|
198
|
+
metadata: {
|
|
199
|
+
source: 'closeRoadmapItemWorkflow',
|
|
200
|
+
project_id: resolvedProjectId,
|
|
201
|
+
workflow_id: resolvedWorkflowId,
|
|
202
|
+
workflow_run_id: resolvedWorkflowRunId,
|
|
203
|
+
task_binding: closureTaskBinding,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw new Error(`claim cannot be established: ${error.message}`);
|
|
208
|
+
}
|
|
209
|
+
effectiveClaimId = cleanText(freshClaim?.claim_id) || cleanText(freshClaim?.claim?.claim_id);
|
|
210
|
+
if (!effectiveClaimId) {
|
|
211
|
+
throw new Error('claim cannot be established.');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const acceptanceCheckResult = await client.getImplementationItemAcceptanceChecks(implementationItemId);
|
|
216
|
+
const acceptanceChecks = Array.isArray(acceptanceCheckResult?.acceptance_checks)
|
|
217
|
+
? acceptanceCheckResult.acceptance_checks.filter((check) => isPlainObject(check))
|
|
218
|
+
: [];
|
|
219
|
+
if (acceptanceChecks.length === 0) {
|
|
220
|
+
throw new Error('acceptance checks are incomplete.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const normalizedAcceptanceStatus = cleanText(requiredAcceptanceCheckStatus) || 'verified';
|
|
224
|
+
const verifiedAcceptanceChecks = [];
|
|
225
|
+
for (const check of acceptanceChecks) {
|
|
226
|
+
const acceptanceCheckId = cleanText(check.acceptance_check_id) || cleanText(check.acceptanceCheckId);
|
|
227
|
+
if (!acceptanceCheckId) {
|
|
228
|
+
throw new Error('acceptance checks are incomplete.');
|
|
229
|
+
}
|
|
230
|
+
const status = cleanText(check.status).toLowerCase();
|
|
231
|
+
if (status !== normalizedAcceptanceStatus.toLowerCase()) {
|
|
232
|
+
try {
|
|
233
|
+
const verification = await client.updateAcceptanceCheckStatusVerified(
|
|
234
|
+
implementationItemId,
|
|
235
|
+
acceptanceCheckId,
|
|
236
|
+
normalizedAcceptanceStatus,
|
|
237
|
+
{
|
|
238
|
+
updated_by: normalizedActorId,
|
|
239
|
+
workflowId: resolvedWorkflowId,
|
|
240
|
+
claim_id: effectiveClaimId,
|
|
241
|
+
status_reason: 'Roadmap closure workflow verified the required acceptance check.',
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
verifiedAcceptanceChecks.push({
|
|
245
|
+
acceptance_check_id: acceptanceCheckId,
|
|
246
|
+
status: normalizedAcceptanceStatus,
|
|
247
|
+
verification,
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new Error(`acceptance checks are incomplete: ${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
verifiedAcceptanceChecks.push({
|
|
255
|
+
acceptance_check_id: acceptanceCheckId,
|
|
256
|
+
status,
|
|
257
|
+
existing: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const artifacts = await client.verifyImplementationItemArtifacts(implementationItemId, {
|
|
262
|
+
requiredArtifacts: cleanList(requiredArtifacts).length > 0
|
|
263
|
+
? cleanList(requiredArtifacts)
|
|
264
|
+
: ['artifact_manifest', 'decision_packet'],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const evidenceRefs = cleanList(gateEvidenceRefs);
|
|
268
|
+
if (evidenceRefs.length === 0) {
|
|
269
|
+
evidenceRefs.push(closureEvidenceRef || `roadmap-closure:${resolvedProjectId}:${itemKey || implementationItemId}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const gateDecisionResult = await client.createImplementationPacketGateDecision(packetId, {
|
|
273
|
+
gate_type: cleanText(gateType) || 'roadmap_closure',
|
|
274
|
+
decision: cleanText(gateDecision) || 'pass',
|
|
275
|
+
rationale: cleanText(gateRationale) || `Roadmap item ${itemKey || implementationItemId} satisfied all closure checks.`,
|
|
276
|
+
evidence_refs: evidenceRefs,
|
|
277
|
+
remediation_actions: cleanList(remediationActions),
|
|
278
|
+
decided_by: normalizedActorId,
|
|
279
|
+
claim_id: effectiveClaimId,
|
|
280
|
+
workflow_id: resolvedWorkflowId,
|
|
281
|
+
workflow_run_id: resolvedWorkflowRunId,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const itemStatusResult = await client.updateImplementationItemStatusVerified(
|
|
285
|
+
implementationItemId,
|
|
286
|
+
cleanText(terminalItemStatus) || 'accepted',
|
|
287
|
+
{
|
|
288
|
+
workflowId: resolvedWorkflowId,
|
|
289
|
+
updated_by: normalizedActorId,
|
|
290
|
+
claim_id: effectiveClaimId,
|
|
291
|
+
status_reason: cleanText(closeProjectReason) || `Roadmap closure workflow advanced ${itemKey || implementationItemId} to terminal status.`,
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const closureEvidenceRefValue = cleanText(closureEvidenceRef) || `roadmap-closure:${resolvedProjectId}:${itemKey || implementationItemId}`;
|
|
296
|
+
const closureEvidence = await client.addImplementationItemEvidence(implementationItemId, {
|
|
297
|
+
evidence_type: cleanText(closureEvidenceType) || 'roadmap_closure',
|
|
298
|
+
evidence_ref: closureEvidenceRefValue,
|
|
299
|
+
title: cleanText(closureEvidenceTitle) || `Closure evidence for ${itemKey || implementationItemId}`,
|
|
300
|
+
recorded_by: normalizedActorId,
|
|
301
|
+
metadata: {
|
|
302
|
+
project_id: resolvedProjectId,
|
|
303
|
+
workflow_id: resolvedWorkflowId,
|
|
304
|
+
workflow_run_id: resolvedWorkflowRunId,
|
|
305
|
+
claim_id: effectiveClaimId,
|
|
306
|
+
gate_decision_id: cleanText(gateDecisionResult?.implementation_gate_decision_id) || cleanText(gateDecisionResult?.gate_decision_id),
|
|
307
|
+
gate_type: cleanText(gateType) || 'roadmap_closure',
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const signedClaim = await client.signoffClaim(effectiveClaimId, {
|
|
312
|
+
actor_signature: normalizedActorId,
|
|
313
|
+
intent_confirmation: cleanText(executionIntent) || 'I confirm this governed roadmap closure workflow.',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const reloadedActiveItem = await client.getProjectRoadmapActiveItem(resolvedProjectId);
|
|
317
|
+
const remainingOpenTasksPayload = await client.listProjectOpenTasks(resolvedProjectId);
|
|
318
|
+
const remainingOpenTasks = Array.isArray(remainingOpenTasksPayload?.tasks)
|
|
319
|
+
? remainingOpenTasksPayload.tasks.filter((task) => isPlainObject(task))
|
|
320
|
+
: Array.isArray(remainingOpenTasksPayload)
|
|
321
|
+
? remainingOpenTasksPayload.filter((task) => isPlainObject(task))
|
|
322
|
+
: [];
|
|
323
|
+
const reloadedItemStatus = cleanText(reloadedActiveItem?.item_status) || cleanText(reloadedActiveItem?.status) || null;
|
|
324
|
+
const terminalStatuses = new Set([
|
|
325
|
+
cleanText(terminalItemStatus) || 'accepted',
|
|
326
|
+
'accepted',
|
|
327
|
+
'verified',
|
|
328
|
+
'done',
|
|
329
|
+
'completed',
|
|
330
|
+
'closed',
|
|
331
|
+
].map((value) => String(value || '').toLowerCase()));
|
|
332
|
+
const activeItemClosed = !reloadedActiveItem || terminalStatuses.has(String(reloadedItemStatus || '').toLowerCase());
|
|
333
|
+
if (!activeItemClosed) {
|
|
334
|
+
throw new Error('project closure state is ambiguous.');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let closeProjectResult = null;
|
|
338
|
+
if (closeProjectIfNoRemainingOpenItems && remainingOpenTasks.length === 0) {
|
|
339
|
+
closeProjectResult = await client.closeActiveProject(resolvedProjectId, {
|
|
340
|
+
workflowId: resolvedWorkflowId,
|
|
341
|
+
workflowRunIds: resolvedWorkflowRunId ? [resolvedWorkflowRunId] : undefined,
|
|
342
|
+
reason: cleanText(closeProjectReason) || `Roadmap item ${itemKey || implementationItemId} closed.`,
|
|
343
|
+
operatorIdentity: normalizedActorId,
|
|
344
|
+
runTerminalStatus: cleanText(terminalItemStatus) || 'accepted',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
status: closeProjectResult ? 'closed' : 'item_closed',
|
|
350
|
+
project_id: resolvedProjectId,
|
|
351
|
+
workflow_id: resolvedWorkflowId,
|
|
352
|
+
workflow_run_id: resolvedWorkflowRunId,
|
|
353
|
+
claim_id: effectiveClaimId,
|
|
354
|
+
claim_source: claimSource,
|
|
355
|
+
continuation_brief: continuation,
|
|
356
|
+
project: projectSummary,
|
|
357
|
+
active_item_before: normalizedActiveItem,
|
|
358
|
+
active_item_after: reloadedActiveItem,
|
|
359
|
+
acceptance_checks: {
|
|
360
|
+
loaded: acceptanceChecks,
|
|
361
|
+
verified: verifiedAcceptanceChecks,
|
|
362
|
+
},
|
|
363
|
+
artifact_verification: artifacts,
|
|
364
|
+
gate_decision: gateDecisionResult,
|
|
365
|
+
implementation_item_status: itemStatusResult,
|
|
366
|
+
closure_evidence: closureEvidence,
|
|
367
|
+
claim_signoff: signedClaim,
|
|
368
|
+
remaining_open_task_count: remainingOpenTasks.length,
|
|
369
|
+
close_project: closeProjectResult,
|
|
370
|
+
};
|
|
371
|
+
},
|
|
48
372
|
};
|
|
49
373
|
}
|