@acorex-platform/module-software-management-client 0.0.46
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/fesm2022/acorex-platform-module-software-management-client-accept-software-suggestion.command-DVcM5TTv.mjs +83 -0
- package/fesm2022/acorex-platform-module-software-management-client-accept-software-suggestion.command-DVcM5TTv.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-complete-software-phase.activity-C56HhK20.mjs +182 -0
- package/fesm2022/acorex-platform-module-software-management-client-complete-software-phase.activity-C56HhK20.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-contracts.mjs +1196 -0
- package/fesm2022/acorex-platform-module-software-management-client-contracts.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-discovery-kickoff.commands-gB0_htfF.mjs +162 -0
- package/fesm2022/acorex-platform-module-software-management-client-discovery-kickoff.commands-gB0_htfF.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-discovery-report.commands-D2ricLpq.mjs +278 -0
- package/fesm2022/acorex-platform-module-software-management-client-discovery-report.commands-D2ricLpq.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-introduction-exit-message.util-DShFM8MY.mjs +71 -0
- package/fesm2022/acorex-platform-module-software-management-client-introduction-exit-message.util-DShFM8MY.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-introduction-task.activities-BOsJbeJe.mjs +158 -0
- package/fesm2022/acorex-platform-module-software-management-client-introduction-task.activities-BOsJbeJe.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-software-command.util-BsoKQCX5.mjs +9 -0
- package/fesm2022/acorex-platform-module-software-management-client-software-command.util-BsoKQCX5.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client-start-software-definition-flow.command-B0YCgv2V.mjs +124 -0
- package/fesm2022/acorex-platform-module-software-management-client-start-software-definition-flow.command-B0YCgv2V.mjs.map +1 -0
- package/fesm2022/acorex-platform-module-software-management-client.mjs +260 -0
- package/fesm2022/acorex-platform-module-software-management-client.mjs.map +1 -0
- package/package.json +28 -0
- package/types/acorex-platform-module-software-management-client-contracts.d.ts +547 -0
- package/types/acorex-platform-module-software-management-client.d.ts +9 -0
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applications & Platform capability for defining standalone software products.
|
|
3
|
+
* Group ids live in `AXPCommonPermissionGroupKeys`.
|
|
4
|
+
*/
|
|
5
|
+
const AXPSoftwareManagementPermissionKeys = {
|
|
6
|
+
MaintainSoftwareDefinitions: 'ApplicationsPlatform:Permission:MaintainSoftwareDefinitions',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const AXPSoftwareManagementMenuKeys = {
|
|
10
|
+
Root: 'SoftwareManagement:Menu',
|
|
11
|
+
Software: 'SoftwareManagement:Menu:Software',
|
|
12
|
+
DiscoveryItems: 'SoftwareManagement:Menu:DiscoveryItems',
|
|
13
|
+
ScopeItems: 'SoftwareManagement:Menu:ScopeItems',
|
|
14
|
+
Requirements: 'SoftwareManagement:Menu:Requirements',
|
|
15
|
+
AcceptanceCriteria: 'SoftwareManagement:Menu:AcceptanceCriteria',
|
|
16
|
+
BusinessRules: 'SoftwareManagement:Menu:BusinessRules',
|
|
17
|
+
Modules: 'SoftwareManagement:Menu:Modules',
|
|
18
|
+
Features: 'SoftwareManagement:Menu:Features',
|
|
19
|
+
UseCases: 'SoftwareManagement:Menu:UseCases',
|
|
20
|
+
DesignDecisions: 'SoftwareManagement:Menu:DesignDecisions',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const config = {
|
|
24
|
+
i18n: 'software-management',
|
|
25
|
+
module: 'SoftwareManagement',
|
|
26
|
+
};
|
|
27
|
+
const RootConfig = {
|
|
28
|
+
config,
|
|
29
|
+
module: {
|
|
30
|
+
module: 'software-management',
|
|
31
|
+
name: config.module,
|
|
32
|
+
title: `@${config.i18n}:module.title`,
|
|
33
|
+
icon: 'fa-light fa-diagram-project',
|
|
34
|
+
},
|
|
35
|
+
entities: {
|
|
36
|
+
software: {
|
|
37
|
+
name: 'Software',
|
|
38
|
+
source: `${config.module}.Software`,
|
|
39
|
+
title: `@${config.i18n}:entities.software.title`,
|
|
40
|
+
titlePlural: `@${config.i18n}:entities.software.plural`,
|
|
41
|
+
icon: 'fa-light fa-lightbulb',
|
|
42
|
+
},
|
|
43
|
+
discoveryItem: {
|
|
44
|
+
name: 'SoftwareDiscoveryItem',
|
|
45
|
+
source: `${config.module}.SoftwareDiscoveryItem`,
|
|
46
|
+
title: `@${config.i18n}:entities.discovery-item.title`,
|
|
47
|
+
titlePlural: `@${config.i18n}:entities.discovery-item.plural`,
|
|
48
|
+
icon: 'fa-light fa-magnifying-glass',
|
|
49
|
+
},
|
|
50
|
+
scopeItem: {
|
|
51
|
+
name: 'SoftwareScopeItem',
|
|
52
|
+
source: `${config.module}.SoftwareScopeItem`,
|
|
53
|
+
title: `@${config.i18n}:entities.scope-item.title`,
|
|
54
|
+
titlePlural: `@${config.i18n}:entities.scope-item.plural`,
|
|
55
|
+
icon: 'fa-light fa-border-outer',
|
|
56
|
+
},
|
|
57
|
+
requirement: {
|
|
58
|
+
name: 'SoftwareRequirement',
|
|
59
|
+
source: `${config.module}.SoftwareRequirement`,
|
|
60
|
+
title: `@${config.i18n}:entities.requirement.title`,
|
|
61
|
+
titlePlural: `@${config.i18n}:entities.requirement.plural`,
|
|
62
|
+
icon: 'fa-light fa-list-check',
|
|
63
|
+
},
|
|
64
|
+
acceptanceCriterion: {
|
|
65
|
+
name: 'SoftwareAcceptanceCriterion',
|
|
66
|
+
source: `${config.module}.SoftwareAcceptanceCriterion`,
|
|
67
|
+
title: `@${config.i18n}:entities.acceptance-criterion.title`,
|
|
68
|
+
titlePlural: `@${config.i18n}:entities.acceptance-criterion.plural`,
|
|
69
|
+
icon: 'fa-light fa-badge-check',
|
|
70
|
+
},
|
|
71
|
+
businessRule: {
|
|
72
|
+
name: 'SoftwareBusinessRule',
|
|
73
|
+
source: `${config.module}.SoftwareBusinessRule`,
|
|
74
|
+
title: `@${config.i18n}:entities.business-rule.title`,
|
|
75
|
+
titlePlural: `@${config.i18n}:entities.business-rule.plural`,
|
|
76
|
+
icon: 'fa-light fa-scale-balanced',
|
|
77
|
+
},
|
|
78
|
+
module: {
|
|
79
|
+
name: 'SoftwareModule',
|
|
80
|
+
source: `${config.module}.SoftwareModule`,
|
|
81
|
+
title: `@${config.i18n}:entities.module.title`,
|
|
82
|
+
titlePlural: `@${config.i18n}:entities.module.plural`,
|
|
83
|
+
icon: 'fa-light fa-cubes',
|
|
84
|
+
},
|
|
85
|
+
feature: {
|
|
86
|
+
name: 'SoftwareFeature',
|
|
87
|
+
source: `${config.module}.SoftwareFeature`,
|
|
88
|
+
title: `@${config.i18n}:entities.feature.title`,
|
|
89
|
+
titlePlural: `@${config.i18n}:entities.feature.plural`,
|
|
90
|
+
icon: 'fa-light fa-puzzle-piece',
|
|
91
|
+
},
|
|
92
|
+
useCase: {
|
|
93
|
+
name: 'SoftwareUseCase',
|
|
94
|
+
source: `${config.module}.SoftwareUseCase`,
|
|
95
|
+
title: `@${config.i18n}:entities.use-case.title`,
|
|
96
|
+
titlePlural: `@${config.i18n}:entities.use-case.plural`,
|
|
97
|
+
icon: 'fa-light fa-person-walking-arrow-right',
|
|
98
|
+
},
|
|
99
|
+
designDecision: {
|
|
100
|
+
name: 'SoftwareDesignDecision',
|
|
101
|
+
source: `${config.module}.SoftwareDesignDecision`,
|
|
102
|
+
title: `@${config.i18n}:entities.design-decision.title`,
|
|
103
|
+
titlePlural: `@${config.i18n}:entities.design-decision.plural`,
|
|
104
|
+
icon: 'fa-light fa-compass-drafting',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//#region ---- Imports ----
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region ---- Manifest ----
|
|
112
|
+
const SoftwareManagementManifest = {
|
|
113
|
+
name: RootConfig.module.name,
|
|
114
|
+
version: '1.0.0',
|
|
115
|
+
title: RootConfig.module.title,
|
|
116
|
+
icon: RootConfig.module.icon,
|
|
117
|
+
i18n: RootConfig.config.i18n,
|
|
118
|
+
type: 'platform',
|
|
119
|
+
domain: 'platform',
|
|
120
|
+
required: false,
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
|
|
124
|
+
const AXPSoftwareManagementCommandKeys = {
|
|
125
|
+
Software: {
|
|
126
|
+
StartFlow: 'SoftwareManagement.Software:StartFlow',
|
|
127
|
+
CompletePhase: 'SoftwareManagement.Software:CompletePhase',
|
|
128
|
+
AcceptSuggestion: 'SoftwareManagement:AcceptSuggestion',
|
|
129
|
+
GenerateKickoffSheet: 'SoftwareManagement.Software:GenerateKickoffSheet',
|
|
130
|
+
SubmitKickoffSheet: 'SoftwareManagement.Software:SubmitKickoffSheet',
|
|
131
|
+
ExportKickoffPdf: 'SoftwareManagement.Software:ExportKickoffPdf',
|
|
132
|
+
OpenKickoffSheet: 'SoftwareManagement.Software:OpenKickoffSheet',
|
|
133
|
+
ViewPinnedKickoff: 'SoftwareManagement.Software:ViewPinnedKickoff',
|
|
134
|
+
GenerateDiscoveryReport: 'SoftwareManagement.Software:GenerateDiscoveryReport',
|
|
135
|
+
SubmitDiscoveryReport: 'SoftwareManagement.Software:SubmitDiscoveryReport',
|
|
136
|
+
ExportDiscoveryReportPdf: 'SoftwareManagement.Software:ExportDiscoveryReportPdf',
|
|
137
|
+
OpenDiscoveryReport: 'SoftwareManagement.Software:OpenDiscoveryReport',
|
|
138
|
+
LinkDiscoveryQuestionnaire: 'SoftwareManagement.Software:LinkDiscoveryQuestionnaire',
|
|
139
|
+
},
|
|
140
|
+
WorkflowActivity: {
|
|
141
|
+
CompletePhase: 'workflow-activity:complete-software-phase',
|
|
142
|
+
LoadIntroduction: 'workflow-activity:load-software-introduction',
|
|
143
|
+
SaveIntroduction: 'workflow-activity:save-software-introduction',
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const SOFTWARE_PHASES = [
|
|
148
|
+
'introduction',
|
|
149
|
+
'discovery',
|
|
150
|
+
'first-version-scope',
|
|
151
|
+
'behavior-specification',
|
|
152
|
+
'design',
|
|
153
|
+
'delivery-planning',
|
|
154
|
+
];
|
|
155
|
+
const SOFTWARE_DEFINITION_WORKFLOW_NAME = 'software-definition';
|
|
156
|
+
function softwarePhaseIndex(phase) {
|
|
157
|
+
return SOFTWARE_PHASES.indexOf(phase);
|
|
158
|
+
}
|
|
159
|
+
function nextSoftwarePhase(phase) {
|
|
160
|
+
const index = softwarePhaseIndex(phase);
|
|
161
|
+
if (index < 0 || index >= SOFTWARE_PHASES.length - 1) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return SOFTWARE_PHASES[index + 1];
|
|
165
|
+
}
|
|
166
|
+
function isSoftwarePhaseAtLeast(current, required) {
|
|
167
|
+
const currentIndex = softwarePhaseIndex(current);
|
|
168
|
+
const requiredIndex = softwarePhaseIndex(required);
|
|
169
|
+
return currentIndex >= 0 && requiredIndex >= 0 && currentIndex >= requiredIndex;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const SOFTWARE_ORIGINS = ['original', 'assumption', 'suggestion', 'approved-fact'];
|
|
173
|
+
const SOFTWARE_PROPOSED_BY = ['user', 'ai'];
|
|
174
|
+
const SOFTWARE_DISCOVERY_KINDS = [
|
|
175
|
+
'audience',
|
|
176
|
+
'problem',
|
|
177
|
+
'pain',
|
|
178
|
+
'current-practice',
|
|
179
|
+
'constraint',
|
|
180
|
+
'alternative',
|
|
181
|
+
'hypothesis',
|
|
182
|
+
'evidence',
|
|
183
|
+
'unknown',
|
|
184
|
+
];
|
|
185
|
+
const SOFTWARE_SCOPE_KINDS = ['capability', 'out-of-scope', 'success-metric'];
|
|
186
|
+
const SOFTWARE_DESIGN_KINDS = ['data-model', 'ux', 'technical'];
|
|
187
|
+
|
|
188
|
+
const M = (en, fa) => ({ 'en-US': en, 'fa-IR': fa });
|
|
189
|
+
const completeAction = {
|
|
190
|
+
name: 'complete-phase',
|
|
191
|
+
title: '@software-management:actions.complete-phase.title',
|
|
192
|
+
icon: 'fa-light fa-check',
|
|
193
|
+
color: 'success',
|
|
194
|
+
command: { name: 'complete-phase' },
|
|
195
|
+
};
|
|
196
|
+
const dialogCancelAction = {
|
|
197
|
+
title: '@general:actions.cancel.title',
|
|
198
|
+
icon: 'fa-times',
|
|
199
|
+
color: 'default',
|
|
200
|
+
command: { name: 'cancel' },
|
|
201
|
+
};
|
|
202
|
+
const dialogSaveAction = {
|
|
203
|
+
title: '@general:actions.save.title',
|
|
204
|
+
icon: 'fa-check',
|
|
205
|
+
color: 'primary',
|
|
206
|
+
command: { name: 'submit', options: { validate: true } },
|
|
207
|
+
};
|
|
208
|
+
function introductionField(label, path, editor, readonly) {
|
|
209
|
+
const editorNode = editor === 'lookup-editor'
|
|
210
|
+
? {
|
|
211
|
+
type: 'lookup-editor',
|
|
212
|
+
path,
|
|
213
|
+
name: path,
|
|
214
|
+
options: {
|
|
215
|
+
entity: 'SecurityManagement.User',
|
|
216
|
+
textField: 'displayName',
|
|
217
|
+
valueField: 'id',
|
|
218
|
+
look: 'select',
|
|
219
|
+
columns: ['displayName', 'username'],
|
|
220
|
+
readonly,
|
|
221
|
+
disabled: readonly,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
: {
|
|
225
|
+
type: editor,
|
|
226
|
+
path,
|
|
227
|
+
name: path,
|
|
228
|
+
options: {
|
|
229
|
+
readonly,
|
|
230
|
+
disabled: readonly,
|
|
231
|
+
...(editor === 'large-text-editor' ? { rows: 4 } : {}),
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
type: 'grid-item-layout',
|
|
236
|
+
options: { colSpan: 12 },
|
|
237
|
+
children: [
|
|
238
|
+
{
|
|
239
|
+
type: 'form-field',
|
|
240
|
+
options: { label, showLabel: true },
|
|
241
|
+
children: [editorNode],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function introductionDialogLayout(readonly) {
|
|
247
|
+
return {
|
|
248
|
+
type: 'grid-layout',
|
|
249
|
+
mode: readonly ? 'view' : 'edit',
|
|
250
|
+
options: {
|
|
251
|
+
grid: {
|
|
252
|
+
default: {
|
|
253
|
+
columns: 'repeat(12, 1fr)',
|
|
254
|
+
rows: 'repeat(7, auto)',
|
|
255
|
+
gap: '16px',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
children: [
|
|
260
|
+
introductionField('@software-management:entities.software.fields.title.title', 'title', 'text-editor', readonly),
|
|
261
|
+
introductionField('@software-management:entities.software.fields.idea.title', 'idea', 'large-text-editor', readonly),
|
|
262
|
+
introductionField('@software-management:entities.software.fields.initial-problem.title', 'initialProblem', 'large-text-editor', readonly),
|
|
263
|
+
introductionField('@software-management:entities.software.fields.goal.title', 'goal', 'large-text-editor', readonly),
|
|
264
|
+
introductionField('@software-management:entities.software.fields.owner.title', 'ownerId', 'lookup-editor', readonly),
|
|
265
|
+
introductionField('@software-management:entities.software.fields.potential-users.title', 'potentialUsers', 'large-text-editor', readonly),
|
|
266
|
+
introductionField('@software-management:entities.software.fields.open-questions.title', 'openQuestions', 'large-text-editor', readonly),
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function introductionDialogContext() {
|
|
271
|
+
return {
|
|
272
|
+
title: '{{ context.eval("outputs.last.title") }}',
|
|
273
|
+
idea: '{{ context.eval("outputs.last.idea") }}',
|
|
274
|
+
initialProblem: '{{ context.eval("outputs.last.initialProblem") }}',
|
|
275
|
+
goal: '{{ context.eval("outputs.last.goal") }}',
|
|
276
|
+
ownerId: '{{ context.eval("outputs.last.ownerId") }}',
|
|
277
|
+
potentialUsers: '{{ context.eval("outputs.last.potentialUsers") }}',
|
|
278
|
+
openQuestions: '{{ context.eval("outputs.last.openQuestions") }}',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function phaseTask(id, phase, title, description) {
|
|
282
|
+
return {
|
|
283
|
+
id,
|
|
284
|
+
name: 'workflow-activity:human-task',
|
|
285
|
+
title,
|
|
286
|
+
inputs: {
|
|
287
|
+
title: `{{ inputs.softwareTitle }} — ${title}`,
|
|
288
|
+
description,
|
|
289
|
+
assignedUserIds: '{{ inputs.ownerId }}',
|
|
290
|
+
context: {
|
|
291
|
+
softwareId: '{{ inputs.softwareId }}',
|
|
292
|
+
phase,
|
|
293
|
+
entityType: 'SoftwareManagement.Software',
|
|
294
|
+
},
|
|
295
|
+
actions: { suffix: [completeAction] },
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function completePhaseActivity(id, phase) {
|
|
300
|
+
return {
|
|
301
|
+
id,
|
|
302
|
+
name: AXPSoftwareManagementCommandKeys.WorkflowActivity.CompletePhase,
|
|
303
|
+
title: `Complete ${phase}`,
|
|
304
|
+
inputs: {
|
|
305
|
+
softwareId: '{{ inputs.softwareId }}',
|
|
306
|
+
phase,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function failedToast(id, message) {
|
|
311
|
+
return {
|
|
312
|
+
id,
|
|
313
|
+
name: 'workflow-activity:show-toast',
|
|
314
|
+
title: 'Phase not ready',
|
|
315
|
+
inputs: {
|
|
316
|
+
message,
|
|
317
|
+
type: 'warning',
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const softwareDefinitionWorkflowDefinition = {
|
|
322
|
+
schemaVersion: 'acorex-platform-workflow/v1',
|
|
323
|
+
name: SOFTWARE_DEFINITION_WORKFLOW_NAME,
|
|
324
|
+
title: M('Software definition', 'تعریف نرمافزار'),
|
|
325
|
+
description: M('Introduction → discovery → first-version scope → behavior → design → delivery planning. Each complete action is gated; AI is never required.', 'معرفی → کشف → محدوده نسخه اول → رفتار → طراحی → برنامهریزی تحویل. هر تکمیل فاز شرط دارد؛ هوش مصنوعی الزامی نیست.'),
|
|
326
|
+
variables: [],
|
|
327
|
+
inputs: [
|
|
328
|
+
{
|
|
329
|
+
name: 'softwareId',
|
|
330
|
+
title: 'Software ID',
|
|
331
|
+
dataType: 'string',
|
|
332
|
+
interface: { type: 'text-editor', options: {} },
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'softwareTitle',
|
|
336
|
+
title: 'Software title',
|
|
337
|
+
dataType: 'string',
|
|
338
|
+
interface: { type: 'text-editor', options: {} },
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'ownerId',
|
|
342
|
+
title: 'Owner user ID',
|
|
343
|
+
dataType: 'string',
|
|
344
|
+
interface: { type: 'text-editor', options: {} },
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
outputs: [],
|
|
348
|
+
extensions: {},
|
|
349
|
+
graph: {
|
|
350
|
+
startActivityId: 'introductionTask',
|
|
351
|
+
activities: [
|
|
352
|
+
{
|
|
353
|
+
id: 'introductionTask',
|
|
354
|
+
name: 'workflow-activity:human-task',
|
|
355
|
+
title: 'Introduction',
|
|
356
|
+
inputs: {
|
|
357
|
+
title: '{{ inputs.softwareTitle }} — Introduction',
|
|
358
|
+
description: '@software-management:workflows.definition.introduction-task.description',
|
|
359
|
+
assignedUserIds: '{{ inputs.ownerId }}',
|
|
360
|
+
context: {
|
|
361
|
+
softwareId: '{{ inputs.softwareId }}',
|
|
362
|
+
phase: 'introduction',
|
|
363
|
+
entityType: 'SoftwareManagement.Software',
|
|
364
|
+
},
|
|
365
|
+
actions: {
|
|
366
|
+
suffix: [
|
|
367
|
+
{
|
|
368
|
+
name: 'edit-introduction',
|
|
369
|
+
title: '@software-management:actions.edit-introduction.title',
|
|
370
|
+
icon: 'fa-light fa-pen',
|
|
371
|
+
color: 'primary',
|
|
372
|
+
command: { name: 'edit-introduction' },
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'generate-kickoff',
|
|
376
|
+
title: '@software-management:actions.generate-kickoff-sheet.title',
|
|
377
|
+
icon: 'fa-light fa-file-lines',
|
|
378
|
+
color: 'primary',
|
|
379
|
+
command: { name: 'generate-kickoff' },
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'submit-kickoff',
|
|
383
|
+
title: '@software-management:actions.submit-kickoff-sheet.title',
|
|
384
|
+
icon: 'fa-light fa-paper-plane',
|
|
385
|
+
color: 'default',
|
|
386
|
+
command: { name: 'submit-kickoff' },
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: 'view-approval',
|
|
390
|
+
title: '@software-management:actions.view-kickoff-approval.title',
|
|
391
|
+
icon: 'fa-light fa-eye',
|
|
392
|
+
color: 'default',
|
|
393
|
+
command: { name: 'view-approval' },
|
|
394
|
+
},
|
|
395
|
+
completeAction,
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'loadIntroductionForEdit',
|
|
402
|
+
name: AXPSoftwareManagementCommandKeys.WorkflowActivity.LoadIntroduction,
|
|
403
|
+
title: 'Load introduction',
|
|
404
|
+
inputs: { softwareId: '{{ inputs.softwareId }}' },
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: 'editIntroductionDialog',
|
|
408
|
+
name: 'workflow-activity:show-layout-popup',
|
|
409
|
+
title: 'Edit introduction',
|
|
410
|
+
inputs: {
|
|
411
|
+
title: '@software-management:actions.edit-introduction.title',
|
|
412
|
+
size: 'lg',
|
|
413
|
+
context: introductionDialogContext(),
|
|
414
|
+
layout: introductionDialogLayout(false),
|
|
415
|
+
actions: { suffix: [dialogCancelAction, dialogSaveAction] },
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
id: 'viewIntroductionDialog',
|
|
420
|
+
name: 'workflow-activity:show-layout-popup',
|
|
421
|
+
title: 'View introduction',
|
|
422
|
+
inputs: {
|
|
423
|
+
title: '@software-management:actions.edit-introduction.title',
|
|
424
|
+
size: 'lg',
|
|
425
|
+
context: introductionDialogContext(),
|
|
426
|
+
layout: introductionDialogLayout(true),
|
|
427
|
+
actions: { suffix: [dialogCancelAction] },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: 'saveIntroductionFromTask',
|
|
432
|
+
name: AXPSoftwareManagementCommandKeys.WorkflowActivity.SaveIntroduction,
|
|
433
|
+
title: 'Save introduction',
|
|
434
|
+
inputs: {
|
|
435
|
+
softwareId: '{{ inputs.softwareId }}',
|
|
436
|
+
title: '{{ context.eval("outputs.last.title") }}',
|
|
437
|
+
idea: '{{ context.eval("outputs.last.idea") }}',
|
|
438
|
+
initialProblem: '{{ context.eval("outputs.last.initialProblem") }}',
|
|
439
|
+
goal: '{{ context.eval("outputs.last.goal") }}',
|
|
440
|
+
ownerId: '{{ context.eval("outputs.last.ownerId") }}',
|
|
441
|
+
potentialUsers: '{{ context.eval("outputs.last.potentialUsers") }}',
|
|
442
|
+
openQuestions: '{{ context.eval("outputs.last.openQuestions") }}',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: 'generateKickoffFromTask',
|
|
447
|
+
name: AXPSoftwareManagementCommandKeys.Software.GenerateKickoffSheet,
|
|
448
|
+
title: 'Generate kickoff sheet',
|
|
449
|
+
inputs: { softwareId: '{{ inputs.softwareId }}' },
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
id: 'submitKickoffFromTask',
|
|
453
|
+
name: AXPSoftwareManagementCommandKeys.Software.SubmitKickoffSheet,
|
|
454
|
+
title: 'Submit kickoff sheet',
|
|
455
|
+
inputs: { softwareId: '{{ inputs.softwareId }}' },
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: 'loadIntroductionForStatus',
|
|
459
|
+
name: AXPSoftwareManagementCommandKeys.WorkflowActivity.LoadIntroduction,
|
|
460
|
+
title: 'Load approval status',
|
|
461
|
+
inputs: { softwareId: '{{ inputs.softwareId }}' },
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: 'approvalStatusDialog',
|
|
465
|
+
name: 'workflow-activity:show-layout-popup',
|
|
466
|
+
title: 'Kickoff approval status',
|
|
467
|
+
inputs: {
|
|
468
|
+
title: '@software-management:actions.view-kickoff-approval.title',
|
|
469
|
+
size: 'md',
|
|
470
|
+
context: {
|
|
471
|
+
approvalText: '{{ context.eval("outputs.last.approvalText") }}',
|
|
472
|
+
},
|
|
473
|
+
layout: {
|
|
474
|
+
type: 'grid-layout',
|
|
475
|
+
mode: 'view',
|
|
476
|
+
options: {
|
|
477
|
+
grid: { default: { columns: 'repeat(12, 1fr)', rows: 'repeat(1, auto)', gap: '16px' } },
|
|
478
|
+
},
|
|
479
|
+
children: [
|
|
480
|
+
introductionField('@software-management:workflows.definition.approval-status.label', 'approvalText', 'large-text-editor', true),
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
actions: { suffix: [dialogCancelAction] },
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
completePhaseActivity('completeIntroduction', 'introduction'),
|
|
487
|
+
phaseTask('discoveryTask', 'discovery', 'Discovery', 'Read the approved kickoff version, collect evidence, then generate and approve a discovery report. Proceed enters scope only after that approval. Revise and stop stay in discovery.'),
|
|
488
|
+
completePhaseActivity('completeDiscovery', 'discovery'),
|
|
489
|
+
failedToast('discoveryFailedToast', 'Discovery cannot enter scope without an approved report, a recorded rationale, and validated user or problem evidence.'),
|
|
490
|
+
phaseTask('scopeTask', 'first-version-scope', 'First-version scope', 'Record essential capabilities, out-of-scope items, and success metrics.'),
|
|
491
|
+
completePhaseActivity('completeScope', 'first-version-scope'),
|
|
492
|
+
failedToast('scopeFailedToast', 'First-version scope cannot complete until discovery is approved.'),
|
|
493
|
+
phaseTask('behaviorTask', 'behavior-specification', 'Behavior specification', 'Record requirements, use cases, business rules, and acceptance criteria.'),
|
|
494
|
+
completePhaseActivity('completeBehavior', 'behavior-specification'),
|
|
495
|
+
failedToast('behaviorFailedToast', 'Behavior specification cannot complete until first-version scope is approved.'),
|
|
496
|
+
phaseTask('designTask', 'design', 'Design', 'Record data-model, UX, and technical decisions. Do not pick a stack before this phase.'),
|
|
497
|
+
completePhaseActivity('completeDesign', 'design'),
|
|
498
|
+
failedToast('designFailedToast', 'Design cannot complete until behavior specification is approved.'),
|
|
499
|
+
phaseTask('deliveryTask', 'delivery-planning', 'Delivery planning', 'Link an existing Project and tasks only after the Software definition exists. Do not clone Project or Task.'),
|
|
500
|
+
completePhaseActivity('completeDelivery', 'delivery-planning'),
|
|
501
|
+
failedToast('deliveryFailedToast', 'Delivery planning cannot complete until design is approved.'),
|
|
502
|
+
{
|
|
503
|
+
id: 'definitionFinishedToast',
|
|
504
|
+
name: 'workflow-activity:show-toast',
|
|
505
|
+
title: 'Definition complete',
|
|
506
|
+
inputs: {
|
|
507
|
+
message: 'Software definition phases are complete. Implementation of the target product is still out of this module.',
|
|
508
|
+
type: 'success',
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
connections: [
|
|
513
|
+
{ source: { activtyName: 'introductionTask', port: 'edit-introduction' }, target: { activtyName: 'loadIntroductionForEdit' } },
|
|
514
|
+
{ source: { activtyName: 'loadIntroductionForEdit', port: 'edit' }, target: { activtyName: 'editIntroductionDialog' } },
|
|
515
|
+
{ source: { activtyName: 'loadIntroductionForEdit', port: 'view' }, target: { activtyName: 'viewIntroductionDialog' } },
|
|
516
|
+
{ source: { activtyName: 'loadIntroductionForEdit', port: 'Failed' }, target: { activtyName: 'introductionTask' } },
|
|
517
|
+
{ source: { activtyName: 'editIntroductionDialog', port: 'submit' }, target: { activtyName: 'saveIntroductionFromTask' } },
|
|
518
|
+
{ source: { activtyName: 'editIntroductionDialog', port: 'cancel' }, target: { activtyName: 'introductionTask' } },
|
|
519
|
+
{ source: { activtyName: 'viewIntroductionDialog', port: 'cancel' }, target: { activtyName: 'introductionTask' } },
|
|
520
|
+
{ source: { activtyName: 'saveIntroductionFromTask', port: 'Done' }, target: { activtyName: 'introductionTask' } },
|
|
521
|
+
{ source: { activtyName: 'saveIntroductionFromTask', port: 'Failed' }, target: { activtyName: 'introductionTask' } },
|
|
522
|
+
{ source: { activtyName: 'introductionTask', port: 'generate-kickoff' }, target: { activtyName: 'generateKickoffFromTask' } },
|
|
523
|
+
{ source: { activtyName: 'generateKickoffFromTask', port: 'Done' }, target: { activtyName: 'introductionTask' } },
|
|
524
|
+
{ source: { activtyName: 'generateKickoffFromTask', port: 'Failed' }, target: { activtyName: 'introductionTask' } },
|
|
525
|
+
{ source: { activtyName: 'introductionTask', port: 'submit-kickoff' }, target: { activtyName: 'submitKickoffFromTask' } },
|
|
526
|
+
{ source: { activtyName: 'submitKickoffFromTask', port: 'Done' }, target: { activtyName: 'introductionTask' } },
|
|
527
|
+
{ source: { activtyName: 'submitKickoffFromTask', port: 'Failed' }, target: { activtyName: 'introductionTask' } },
|
|
528
|
+
{ source: { activtyName: 'introductionTask', port: 'view-approval' }, target: { activtyName: 'loadIntroductionForStatus' } },
|
|
529
|
+
{ source: { activtyName: 'loadIntroductionForStatus', port: 'edit' }, target: { activtyName: 'approvalStatusDialog' } },
|
|
530
|
+
{ source: { activtyName: 'loadIntroductionForStatus', port: 'view' }, target: { activtyName: 'approvalStatusDialog' } },
|
|
531
|
+
{ source: { activtyName: 'loadIntroductionForStatus', port: 'Failed' }, target: { activtyName: 'introductionTask' } },
|
|
532
|
+
{ source: { activtyName: 'approvalStatusDialog', port: 'cancel' }, target: { activtyName: 'introductionTask' } },
|
|
533
|
+
{ source: { activtyName: 'introductionTask', port: 'complete-phase' }, target: { activtyName: 'completeIntroduction' } },
|
|
534
|
+
{ source: { activtyName: 'completeIntroduction', port: 'Done' }, target: { activtyName: 'discoveryTask' } },
|
|
535
|
+
{ source: { activtyName: 'completeIntroduction', port: 'Failed' }, target: { activtyName: 'introductionTask' } },
|
|
536
|
+
{ source: { activtyName: 'discoveryTask', port: 'complete-phase' }, target: { activtyName: 'completeDiscovery' } },
|
|
537
|
+
{ source: { activtyName: 'completeDiscovery', port: 'Done' }, target: { activtyName: 'scopeTask' } },
|
|
538
|
+
{ source: { activtyName: 'completeDiscovery', port: 'Failed' }, target: { activtyName: 'discoveryFailedToast' } },
|
|
539
|
+
{ source: { activtyName: 'discoveryFailedToast', port: 'Done' }, target: { activtyName: 'discoveryTask' } },
|
|
540
|
+
{ source: { activtyName: 'scopeTask', port: 'complete-phase' }, target: { activtyName: 'completeScope' } },
|
|
541
|
+
{ source: { activtyName: 'completeScope', port: 'Done' }, target: { activtyName: 'behaviorTask' } },
|
|
542
|
+
{ source: { activtyName: 'completeScope', port: 'Failed' }, target: { activtyName: 'scopeFailedToast' } },
|
|
543
|
+
{ source: { activtyName: 'scopeFailedToast', port: 'Done' }, target: { activtyName: 'scopeTask' } },
|
|
544
|
+
{ source: { activtyName: 'behaviorTask', port: 'complete-phase' }, target: { activtyName: 'completeBehavior' } },
|
|
545
|
+
{ source: { activtyName: 'completeBehavior', port: 'Done' }, target: { activtyName: 'designTask' } },
|
|
546
|
+
{ source: { activtyName: 'completeBehavior', port: 'Failed' }, target: { activtyName: 'behaviorFailedToast' } },
|
|
547
|
+
{ source: { activtyName: 'behaviorFailedToast', port: 'Done' }, target: { activtyName: 'behaviorTask' } },
|
|
548
|
+
{ source: { activtyName: 'designTask', port: 'complete-phase' }, target: { activtyName: 'completeDesign' } },
|
|
549
|
+
{ source: { activtyName: 'completeDesign', port: 'Done' }, target: { activtyName: 'deliveryTask' } },
|
|
550
|
+
{ source: { activtyName: 'completeDesign', port: 'Failed' }, target: { activtyName: 'designFailedToast' } },
|
|
551
|
+
{ source: { activtyName: 'designFailedToast', port: 'Done' }, target: { activtyName: 'designTask' } },
|
|
552
|
+
{ source: { activtyName: 'deliveryTask', port: 'complete-phase' }, target: { activtyName: 'completeDelivery' } },
|
|
553
|
+
{ source: { activtyName: 'completeDelivery', port: 'Done' }, target: { activtyName: 'definitionFinishedToast' } },
|
|
554
|
+
{ source: { activtyName: 'completeDelivery', port: 'Failed' }, target: { activtyName: 'deliveryFailedToast' } },
|
|
555
|
+
{ source: { activtyName: 'deliveryFailedToast', port: 'Done' }, target: { activtyName: 'deliveryTask' } },
|
|
556
|
+
],
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
//#region ----- Constants -----
|
|
561
|
+
/** Document type name in Document Management. One kickoff sheet per Software record. */
|
|
562
|
+
const DISCOVERY_KICKOFF_DOCUMENT_TYPE_NAME = 'software-discovery-kickoff';
|
|
563
|
+
const DISCOVERY_KICKOFF_FILE_NAME = 'discovery-kickoff.html';
|
|
564
|
+
/**
|
|
565
|
+
* Review statuses stored on DocumentManagement.Document.
|
|
566
|
+
* Same vocabulary as the EDMS document review workflow.
|
|
567
|
+
*/
|
|
568
|
+
const KICKOFF_REVIEW_STATUS = {
|
|
569
|
+
Draft: 'draft',
|
|
570
|
+
InReview: 'in-review',
|
|
571
|
+
Approved: 'approved',
|
|
572
|
+
Rejected: 'rejected',
|
|
573
|
+
RevisionRequested: 'revision-requested',
|
|
574
|
+
};
|
|
575
|
+
const KICKOFF_REVIEW_REASON_BASE_DATA = 'introduction-fields-changed';
|
|
576
|
+
/** Fields whose change must not silently rewrite an approved sheet. */
|
|
577
|
+
const KICKOFF_SOURCE_FIELDS = [
|
|
578
|
+
'title',
|
|
579
|
+
'idea',
|
|
580
|
+
'originalIdea',
|
|
581
|
+
'initialProblem',
|
|
582
|
+
'goal',
|
|
583
|
+
'ownerId',
|
|
584
|
+
'potentialUsers',
|
|
585
|
+
'openQuestions',
|
|
586
|
+
];
|
|
587
|
+
const INTRODUCTION_REQUIRED_FIELDS = ['title', 'idea', 'initialProblem', 'goal', 'ownerId'];
|
|
588
|
+
const INTRODUCTION_EXIT_MESSAGE = {
|
|
589
|
+
missingFields: '@software-management:messages.introduction.missing-fields',
|
|
590
|
+
kickoffRejected: '@software-management:messages.introduction.kickoff-rejected',
|
|
591
|
+
kickoffRevision: '@software-management:messages.introduction.kickoff-revision',
|
|
592
|
+
kickoffInReview: '@software-management:messages.introduction.kickoff-in-review',
|
|
593
|
+
kickoffDraft: '@software-management:messages.introduction.kickoff-draft',
|
|
594
|
+
kickoffMissing: '@software-management:messages.introduction.kickoff-missing',
|
|
595
|
+
kickoffRequired: '@software-management:messages.introduction.kickoff-required',
|
|
596
|
+
kickoffPinMissing: '@software-management:messages.introduction.kickoff-pin-missing',
|
|
597
|
+
kickoffFingerprintMissing: '@software-management:messages.introduction.kickoff-fingerprint-missing',
|
|
598
|
+
kickoffStale: '@software-management:messages.introduction.kickoff-stale',
|
|
599
|
+
kickoffNotApproved: '@software-management:messages.introduction.kickoff-not-approved',
|
|
600
|
+
kickoffVersionMismatch: '@software-management:messages.introduction.kickoff-version-mismatch',
|
|
601
|
+
ready: '@software-management:messages.introduction.ready',
|
|
602
|
+
saved: '@software-management:messages.introduction.saved',
|
|
603
|
+
};
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region ----- Labels -----
|
|
606
|
+
const CERTAINTY_LABEL = {
|
|
607
|
+
recorded: { en: 'Recorded', fa: 'ثبتشده' },
|
|
608
|
+
hypothesis: { en: 'Hypothesis', fa: 'فرضیه' },
|
|
609
|
+
unknown: { en: 'Unknown', fa: 'نامعلوم' },
|
|
610
|
+
};
|
|
611
|
+
const UNKNOWN_BODY = 'Unknown / نامعلوم';
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region ----- Text helpers -----
|
|
614
|
+
function kickoffText(value) {
|
|
615
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
616
|
+
}
|
|
617
|
+
/** Review status stored as a string or as a lookup `{ id }`. */
|
|
618
|
+
function kickoffStatusId(value) {
|
|
619
|
+
const direct = kickoffText(value);
|
|
620
|
+
if (direct) {
|
|
621
|
+
return direct;
|
|
622
|
+
}
|
|
623
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
624
|
+
return '';
|
|
625
|
+
}
|
|
626
|
+
return kickoffText(value['id']);
|
|
627
|
+
}
|
|
628
|
+
const LOOKUP_STUB_KEYS = new Set(['id', 'displayName', 'title', 'name', 'username', 'fullName']);
|
|
629
|
+
/**
|
|
630
|
+
* Reads introduction text from a string, a locale map, or a lookup stub such as `{ id, displayName }`.
|
|
631
|
+
* A lookup stub contributes its id. Other objects contribute the first non-empty locale string.
|
|
632
|
+
*/
|
|
633
|
+
function introductionValueText(value) {
|
|
634
|
+
const direct = kickoffText(value);
|
|
635
|
+
if (direct) {
|
|
636
|
+
return direct;
|
|
637
|
+
}
|
|
638
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
639
|
+
return '';
|
|
640
|
+
}
|
|
641
|
+
const record = value;
|
|
642
|
+
const id = kickoffText(record['id']);
|
|
643
|
+
const keys = Object.keys(record);
|
|
644
|
+
if (id && keys.length > 0 && keys.every((key) => LOOKUP_STUB_KEYS.has(key))) {
|
|
645
|
+
return id;
|
|
646
|
+
}
|
|
647
|
+
const preferred = kickoffText(record['en-US']) || kickoffText(record['fa-IR']) || kickoffText(record['en-AU']);
|
|
648
|
+
if (preferred) {
|
|
649
|
+
return preferred;
|
|
650
|
+
}
|
|
651
|
+
for (const entry of Object.values(record)) {
|
|
652
|
+
const text = kickoffText(entry);
|
|
653
|
+
if (text) {
|
|
654
|
+
return text;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return '';
|
|
658
|
+
}
|
|
659
|
+
function missingIntroductionFields(software) {
|
|
660
|
+
return INTRODUCTION_REQUIRED_FIELDS.filter((field) => !introductionValueText(software[field]));
|
|
661
|
+
}
|
|
662
|
+
function escapeHtml$1(value) {
|
|
663
|
+
return value
|
|
664
|
+
.replace(/&/g, '&')
|
|
665
|
+
.replace(/</g, '<')
|
|
666
|
+
.replace(/>/g, '>')
|
|
667
|
+
.replace(/"/g, '"');
|
|
668
|
+
}
|
|
669
|
+
function isAiDraft(source) {
|
|
670
|
+
return kickoffText(source.proposedBy) === 'ai' || kickoffText(source.origin) === 'suggestion';
|
|
671
|
+
}
|
|
672
|
+
function narrativeCertainty(source, body) {
|
|
673
|
+
if (!body) {
|
|
674
|
+
return 'unknown';
|
|
675
|
+
}
|
|
676
|
+
return isAiDraft(source) ? 'hypothesis' : 'recorded';
|
|
677
|
+
}
|
|
678
|
+
function openCertainty(body) {
|
|
679
|
+
return body ? 'hypothesis' : 'unknown';
|
|
680
|
+
}
|
|
681
|
+
function section(key, titleEn, titleFa, body, certainty) {
|
|
682
|
+
return {
|
|
683
|
+
key,
|
|
684
|
+
titleEn,
|
|
685
|
+
titleFa,
|
|
686
|
+
body: body || UNKNOWN_BODY,
|
|
687
|
+
certainty,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region ----- Fingerprint -----
|
|
692
|
+
function sourcePayload(source) {
|
|
693
|
+
return [
|
|
694
|
+
introductionValueText(source.title),
|
|
695
|
+
introductionValueText(source.idea),
|
|
696
|
+
introductionValueText(source.originalIdea),
|
|
697
|
+
introductionValueText(source.initialProblem),
|
|
698
|
+
introductionValueText(source.goal),
|
|
699
|
+
introductionValueText(source.ownerId),
|
|
700
|
+
introductionValueText(source.potentialUsers),
|
|
701
|
+
introductionValueText(source.openQuestions),
|
|
702
|
+
kickoffText(source.proposedBy),
|
|
703
|
+
kickoffText(source.origin),
|
|
704
|
+
].join('\n');
|
|
705
|
+
}
|
|
706
|
+
/** Stable id of the introduction inputs baked into a sheet version. */
|
|
707
|
+
function discoveryKickoffFingerprint(source) {
|
|
708
|
+
const payload = sourcePayload(source);
|
|
709
|
+
let hash = 0x811c9dc5;
|
|
710
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
711
|
+
hash = Math.imul(hash ^ payload.charCodeAt(index), 0x01000193);
|
|
712
|
+
}
|
|
713
|
+
const hex = (hash >>> 0).toString(16).padStart(8, '0');
|
|
714
|
+
return `kickoff-${hex}-${payload.length}`;
|
|
715
|
+
}
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region ----- Sheet -----
|
|
718
|
+
function kickoffSourceFromSoftware(software) {
|
|
719
|
+
const ownerName = kickoffText(software.ownerName) || kickoffText(software.owner?.displayName);
|
|
720
|
+
return {
|
|
721
|
+
title: software.title,
|
|
722
|
+
idea: software.idea,
|
|
723
|
+
originalIdea: software.originalIdea,
|
|
724
|
+
initialProblem: software.initialProblem,
|
|
725
|
+
goal: software.goal,
|
|
726
|
+
ownerId: software.ownerId,
|
|
727
|
+
ownerName,
|
|
728
|
+
potentialUsers: software.potentialUsers,
|
|
729
|
+
openQuestions: software.openQuestions,
|
|
730
|
+
proposedBy: software.proposedBy,
|
|
731
|
+
origin: software.origin,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Builds the discovery kickoff sheet from introduction fields only.
|
|
736
|
+
* Empty research items stay Unknown. Author-supplied guesses stay Hypothesis.
|
|
737
|
+
* AI text is a draft: it is never labeled as evidence or a confirmed fact.
|
|
738
|
+
*/
|
|
739
|
+
function buildDiscoveryKickoffSheet(source) {
|
|
740
|
+
const aiDraft = isAiDraft(source);
|
|
741
|
+
const title = introductionValueText(source.title);
|
|
742
|
+
const idea = introductionValueText(source.idea);
|
|
743
|
+
const originalIdea = introductionValueText(source.originalIdea) || idea;
|
|
744
|
+
const problem = introductionValueText(source.initialProblem);
|
|
745
|
+
const goal = introductionValueText(source.goal);
|
|
746
|
+
const owner = kickoffText(source.ownerName) || introductionValueText(source.ownerId);
|
|
747
|
+
const potentialUsers = introductionValueText(source.potentialUsers);
|
|
748
|
+
const openQuestions = introductionValueText(source.openQuestions);
|
|
749
|
+
const sections = [
|
|
750
|
+
section('title', 'Title', 'عنوان', title, narrativeCertainty(source, title)),
|
|
751
|
+
section('original-idea', 'Original Idea at Registration', 'ایدهٔ اولیه هنگام ثبت', originalIdea, narrativeCertainty(source, originalIdea)),
|
|
752
|
+
section('idea', 'Idea Description', 'شرح ایده', idea, narrativeCertainty(source, idea)),
|
|
753
|
+
section('initial-problem', 'Initial Problem', 'مسئلهٔ اولیه', problem, narrativeCertainty(source, problem)),
|
|
754
|
+
section('goal', 'Goal', 'هدف', goal, narrativeCertainty(source, goal)),
|
|
755
|
+
section('owner', 'Owner', 'مسئول', owner, owner ? 'recorded' : 'unknown'),
|
|
756
|
+
section('potential-users', 'Potential Users', 'کاربران احتمالی', potentialUsers, openCertainty(potentialUsers)),
|
|
757
|
+
section('open-questions', 'Open Research Questions', 'پرسشهای باز تحقیق', openQuestions, openCertainty(openQuestions)),
|
|
758
|
+
];
|
|
759
|
+
const fingerprint = discoveryKickoffFingerprint(source);
|
|
760
|
+
const plainText = renderPlainText(title, sections, aiDraft, fingerprint);
|
|
761
|
+
const html = renderHtml(title, sections, aiDraft, fingerprint);
|
|
762
|
+
return {
|
|
763
|
+
title: title || 'Discovery Kickoff Sheet',
|
|
764
|
+
fingerprint,
|
|
765
|
+
html,
|
|
766
|
+
plainText,
|
|
767
|
+
sections,
|
|
768
|
+
aiDraft,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function renderPlainText(title, sections, aiDraft, fingerprint) {
|
|
772
|
+
const lines = [
|
|
773
|
+
'Discovery Kickoff Sheet / برگهٔ آغاز کشف',
|
|
774
|
+
title ? `Title / عنوان: ${title}` : '',
|
|
775
|
+
'Unconfirmed items are marked Hypothesis or Unknown. This sheet is not evidence.',
|
|
776
|
+
'موارد تأییدنشده فرضیه یا نامعلوماند. این برگه شاهد نیست.',
|
|
777
|
+
];
|
|
778
|
+
if (aiDraft) {
|
|
779
|
+
lines.push('AI draft only. This text is not evidence and is not a confirmed fact.', 'فقط پیشنویس هوش مصنوعی. این متن شاهد یا واقعیت تأییدشده نیست.');
|
|
780
|
+
}
|
|
781
|
+
for (const item of sections) {
|
|
782
|
+
const label = CERTAINTY_LABEL[item.certainty];
|
|
783
|
+
lines.push('', `${item.titleEn} / ${item.titleFa} [${label.en} / ${label.fa}]`, item.body);
|
|
784
|
+
}
|
|
785
|
+
lines.push('', `Fingerprint: ${fingerprint}`);
|
|
786
|
+
return lines.filter((line) => line !== undefined).join('\n');
|
|
787
|
+
}
|
|
788
|
+
function renderHtml(title, sections, aiDraft, fingerprint) {
|
|
789
|
+
const blocks = sections
|
|
790
|
+
.map((item) => {
|
|
791
|
+
const label = CERTAINTY_LABEL[item.certainty];
|
|
792
|
+
return [
|
|
793
|
+
'<section>',
|
|
794
|
+
`<h2>${escapeHtml$1(item.titleEn)} / ${escapeHtml$1(item.titleFa)}</h2>`,
|
|
795
|
+
`<p class="certainty">${escapeHtml$1(label.en)} / ${escapeHtml$1(label.fa)}</p>`,
|
|
796
|
+
`<p>${escapeHtml$1(item.body).replace(/\n/g, '<br>')}</p>`,
|
|
797
|
+
'</section>',
|
|
798
|
+
].join('');
|
|
799
|
+
})
|
|
800
|
+
.join('');
|
|
801
|
+
const aiNote = aiDraft
|
|
802
|
+
? '<p class="banner">AI draft only. This text is not evidence and is not a confirmed fact. / فقط پیشنویس هوش مصنوعی. این متن شاهد یا واقعیت تأییدشده نیست.</p>'
|
|
803
|
+
: '';
|
|
804
|
+
return [
|
|
805
|
+
'<!DOCTYPE html>',
|
|
806
|
+
'<html lang="fa">',
|
|
807
|
+
'<head>',
|
|
808
|
+
'<meta charset="utf-8">',
|
|
809
|
+
`<title>${escapeHtml$1(title || 'Discovery Kickoff Sheet')}</title>`,
|
|
810
|
+
'<style>',
|
|
811
|
+
'body{font-family:Segoe UI,Tahoma,sans-serif;line-height:1.5;margin:2rem;color:#1a1a1a;}',
|
|
812
|
+
'h1{font-size:1.6rem;} h2{font-size:1.1rem;margin-bottom:0.2rem;}',
|
|
813
|
+
'.banner{background:#fff6d8;border:1px solid #e6d38a;padding:0.75rem 1rem;}',
|
|
814
|
+
'.certainty{font-size:0.85rem;color:#555;margin-top:0;}',
|
|
815
|
+
'section{margin:1.25rem 0;padding-top:0.5rem;border-top:1px solid #ddd;}',
|
|
816
|
+
'</style>',
|
|
817
|
+
'</head>',
|
|
818
|
+
'<body>',
|
|
819
|
+
'<h1>Discovery Kickoff Sheet / برگهٔ آغاز کشف</h1>',
|
|
820
|
+
'<p>Unconfirmed items are marked Hypothesis or Unknown. This sheet is not evidence. / موارد تأییدنشده فرضیه یا نامعلوماند. این برگه شاهد نیست.</p>',
|
|
821
|
+
aiNote,
|
|
822
|
+
blocks,
|
|
823
|
+
`<p class="certainty">Fingerprint: ${escapeHtml$1(fingerprint)}</p>`,
|
|
824
|
+
'</body>',
|
|
825
|
+
'</html>',
|
|
826
|
+
].join('');
|
|
827
|
+
}
|
|
828
|
+
function readKickoffMeta(value) {
|
|
829
|
+
if (!value || typeof value !== 'object') {
|
|
830
|
+
return {};
|
|
831
|
+
}
|
|
832
|
+
return value;
|
|
833
|
+
}
|
|
834
|
+
/** Fingerprint stored on the version, with the host document as fallback. */
|
|
835
|
+
function kickoffFingerprintFromRows(version, host) {
|
|
836
|
+
return (kickoffText(readKickoffMeta(version?.metaDataForm).fingerprint) ||
|
|
837
|
+
kickoffText(readKickoffMeta(host?.metaDataForm).fingerprint));
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region ----- Review and phase gate -----
|
|
841
|
+
function kickoffReviewKeepsIntroduction(statusId) {
|
|
842
|
+
return (statusId === KICKOFF_REVIEW_STATUS.Rejected ||
|
|
843
|
+
statusId === KICKOFF_REVIEW_STATUS.RevisionRequested);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Fills the gate check from the document when an approved host version exists
|
|
847
|
+
* but the software row has not stored that version yet.
|
|
848
|
+
* An unapproved sheet never becomes the pinned version here.
|
|
849
|
+
*/
|
|
850
|
+
function buildIntroductionKickoffCheck(input) {
|
|
851
|
+
if (!input) {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
const statusId = kickoffStatusId(input.statusId) || kickoffStatusId(input.status);
|
|
855
|
+
const hostVersionId = kickoffText(input.hostVersionId);
|
|
856
|
+
const versionFingerprint = kickoffText(input.versionFingerprint);
|
|
857
|
+
let approvedVersionId = kickoffText(input.approvedVersionId);
|
|
858
|
+
let approvedFingerprint = kickoffText(input.approvedFingerprint);
|
|
859
|
+
if (!approvedVersionId && !approvedFingerprint && !statusId && !hostVersionId) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
if (statusId === KICKOFF_REVIEW_STATUS.Approved && hostVersionId) {
|
|
863
|
+
if (!approvedVersionId) {
|
|
864
|
+
approvedVersionId = hostVersionId;
|
|
865
|
+
}
|
|
866
|
+
if (!approvedFingerprint && versionFingerprint) {
|
|
867
|
+
approvedFingerprint = versionFingerprint;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return { statusId, hostVersionId, approvedVersionId, approvedFingerprint };
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Introduction may leave only when one specific document version is approved
|
|
874
|
+
* and still matches the current introduction fields.
|
|
875
|
+
* Discovery must keep using `discoveryVersionId`, not a later draft.
|
|
876
|
+
*/
|
|
877
|
+
function evaluateIntroductionExit(software, kickoff) {
|
|
878
|
+
const stay = (message, discoveryVersionId = '', missingFields) => ({
|
|
879
|
+
ok: false,
|
|
880
|
+
message,
|
|
881
|
+
discoveryVersionId,
|
|
882
|
+
...(missingFields?.length ? { missingFields } : {}),
|
|
883
|
+
});
|
|
884
|
+
const missingFields = missingIntroductionFields(software);
|
|
885
|
+
if (missingFields.length) {
|
|
886
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.missingFields, '', missingFields);
|
|
887
|
+
}
|
|
888
|
+
if (!kickoff) {
|
|
889
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffMissing);
|
|
890
|
+
}
|
|
891
|
+
const approvedVersionId = kickoffText(kickoff.approvedVersionId);
|
|
892
|
+
const statusId = kickoffStatusId(kickoff.statusId);
|
|
893
|
+
const hostVersionId = kickoffText(kickoff.hostVersionId);
|
|
894
|
+
const approvedFingerprint = kickoffText(kickoff.approvedFingerprint);
|
|
895
|
+
const currentFingerprint = discoveryKickoffFingerprint(software);
|
|
896
|
+
if (!approvedVersionId) {
|
|
897
|
+
if (kickoffReviewKeepsIntroduction(statusId)) {
|
|
898
|
+
return stay(statusId === KICKOFF_REVIEW_STATUS.Rejected
|
|
899
|
+
? INTRODUCTION_EXIT_MESSAGE.kickoffRejected
|
|
900
|
+
: INTRODUCTION_EXIT_MESSAGE.kickoffRevision);
|
|
901
|
+
}
|
|
902
|
+
if (statusId === KICKOFF_REVIEW_STATUS.InReview) {
|
|
903
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffInReview);
|
|
904
|
+
}
|
|
905
|
+
if (statusId === KICKOFF_REVIEW_STATUS.Draft) {
|
|
906
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffDraft);
|
|
907
|
+
}
|
|
908
|
+
if (statusId === KICKOFF_REVIEW_STATUS.Approved) {
|
|
909
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffPinMissing);
|
|
910
|
+
}
|
|
911
|
+
if (!statusId) {
|
|
912
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffMissing);
|
|
913
|
+
}
|
|
914
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffRequired);
|
|
915
|
+
}
|
|
916
|
+
if (!approvedFingerprint) {
|
|
917
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffFingerprintMissing, approvedVersionId);
|
|
918
|
+
}
|
|
919
|
+
if (approvedFingerprint !== currentFingerprint) {
|
|
920
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffStale, approvedVersionId);
|
|
921
|
+
}
|
|
922
|
+
if (statusId !== KICKOFF_REVIEW_STATUS.Approved) {
|
|
923
|
+
if (kickoffReviewKeepsIntroduction(statusId)) {
|
|
924
|
+
return stay(statusId === KICKOFF_REVIEW_STATUS.Rejected
|
|
925
|
+
? INTRODUCTION_EXIT_MESSAGE.kickoffRejected
|
|
926
|
+
: INTRODUCTION_EXIT_MESSAGE.kickoffRevision, approvedVersionId);
|
|
927
|
+
}
|
|
928
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffNotApproved, approvedVersionId);
|
|
929
|
+
}
|
|
930
|
+
if (!hostVersionId || hostVersionId !== approvedVersionId) {
|
|
931
|
+
return stay(INTRODUCTION_EXIT_MESSAGE.kickoffVersionMismatch, approvedVersionId);
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
ok: true,
|
|
935
|
+
message: '',
|
|
936
|
+
discoveryVersionId: approvedVersionId,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* PDF bytes must be rendered from the approved version's stored HTML.
|
|
941
|
+
* Rebuilding from later introduction fields is not allowed.
|
|
942
|
+
*/
|
|
943
|
+
function approvedKickoffPdfSource(version, approvedVersionId) {
|
|
944
|
+
const versionId = kickoffText(version?.id);
|
|
945
|
+
if (!approvedVersionId || versionId !== approvedVersionId) {
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
html: '',
|
|
949
|
+
message: 'PDF export needs the approved kickoff sheet version.',
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const html = kickoffText(readKickoffMeta(version?.metaDataForm).bodyHtml);
|
|
953
|
+
if (!html) {
|
|
954
|
+
return {
|
|
955
|
+
ok: false,
|
|
956
|
+
html: '',
|
|
957
|
+
message: 'The approved kickoff sheet has no readable HTML to export.',
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
return { ok: true, html, message: '' };
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* A later edit must keep the approved HTML intact and ask for a new version.
|
|
964
|
+
*/
|
|
965
|
+
function kickoffAfterBaseDataChange(approved, next) {
|
|
966
|
+
const nextFingerprint = discoveryKickoffFingerprint(next);
|
|
967
|
+
const changed = nextFingerprint !== approved.fingerprint;
|
|
968
|
+
return {
|
|
969
|
+
approvedHtml: approved.html,
|
|
970
|
+
needsReview: changed,
|
|
971
|
+
reviewReason: changed ? KICKOFF_REVIEW_REASON_BASE_DATA : '',
|
|
972
|
+
nextFingerprint,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
//#endregion
|
|
976
|
+
|
|
977
|
+
//#region ----- Constants -----
|
|
978
|
+
const DISCOVERY_REPORT_DOCUMENT_TYPE_NAME = 'software-discovery-report';
|
|
979
|
+
const DISCOVERY_REPORT_FILE_NAME = 'discovery-report.html';
|
|
980
|
+
const DISCOVERY_DECISIONS = ['proceed', 'revise', 'stop'];
|
|
981
|
+
const DISCOVERY_STANCES = ['assumption', 'validated'];
|
|
982
|
+
const DISCOVERY_METHODS = [
|
|
983
|
+
'interview',
|
|
984
|
+
'observation',
|
|
985
|
+
'analytics',
|
|
986
|
+
'document',
|
|
987
|
+
'questionnaire',
|
|
988
|
+
'other',
|
|
989
|
+
];
|
|
990
|
+
const DISCOVERY_FINDINGS_CHANGED = 'discovery-findings-changed';
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region ----- Findings -----
|
|
993
|
+
function discoveryStance(value) {
|
|
994
|
+
return kickoffText(value) === 'validated' ? 'validated' : 'assumption';
|
|
995
|
+
}
|
|
996
|
+
function parseSourceRefs(value) {
|
|
997
|
+
return kickoffText(value)
|
|
998
|
+
.split(/\r?\n/)
|
|
999
|
+
.map((line) => line.trim())
|
|
1000
|
+
.filter((line) => line.length > 0);
|
|
1001
|
+
}
|
|
1002
|
+
function findingSupportsProceed(finding) {
|
|
1003
|
+
const kind = kickoffText(finding.kind);
|
|
1004
|
+
const userOrProblem = kind === 'audience' || kind === 'problem' || kind === 'pain';
|
|
1005
|
+
const hasEvidence = Boolean(kickoffText(finding.method) || parseSourceRefs(finding.sourceRefs).length);
|
|
1006
|
+
return discoveryStance(finding.stance) === 'validated' && userOrProblem && hasEvidence && Boolean(kickoffText(finding.claim) || kickoffText(finding.title));
|
|
1007
|
+
}
|
|
1008
|
+
function discoveryReportFingerprint(source) {
|
|
1009
|
+
const findings = [...source.findings]
|
|
1010
|
+
.map((finding) => [
|
|
1011
|
+
kickoffText(finding.id),
|
|
1012
|
+
kickoffText(finding.kind),
|
|
1013
|
+
kickoffText(finding.title),
|
|
1014
|
+
kickoffText(finding.body),
|
|
1015
|
+
discoveryStance(finding.stance),
|
|
1016
|
+
kickoffText(finding.method),
|
|
1017
|
+
kickoffText(finding.researchDate),
|
|
1018
|
+
kickoffText(finding.userGroup),
|
|
1019
|
+
kickoffText(finding.claim),
|
|
1020
|
+
parseSourceRefs(finding.sourceRefs).join('|'),
|
|
1021
|
+
kickoffText(finding.kickoffVersionId),
|
|
1022
|
+
].join('\u001f'))
|
|
1023
|
+
.sort();
|
|
1024
|
+
const payload = [source.kickoffVersionId, source.decision, source.rationale, ...findings].join('\u001e');
|
|
1025
|
+
let hash = 0x811c9dc5;
|
|
1026
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
1027
|
+
hash ^= payload.charCodeAt(index);
|
|
1028
|
+
hash = Math.imul(hash, 0x01000193);
|
|
1029
|
+
}
|
|
1030
|
+
return `discovery-${(hash >>> 0).toString(16).padStart(8, '0')}-${payload.length}`;
|
|
1031
|
+
}
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region ----- Report -----
|
|
1034
|
+
function escapeHtml(value) {
|
|
1035
|
+
return value
|
|
1036
|
+
.replace(/&/g, '&')
|
|
1037
|
+
.replace(/</g, '<')
|
|
1038
|
+
.replace(/>/g, '>');
|
|
1039
|
+
}
|
|
1040
|
+
function kickoffPlainTextFromVersion(version) {
|
|
1041
|
+
const meta = readKickoffMeta(version?.metaDataForm);
|
|
1042
|
+
return kickoffText(meta.plainText) || kickoffText(meta.bodyHtml);
|
|
1043
|
+
}
|
|
1044
|
+
function buildDiscoveryReport(source) {
|
|
1045
|
+
const fingerprint = discoveryReportFingerprint(source);
|
|
1046
|
+
const lines = [
|
|
1047
|
+
'Discovery Report / گزارش کشف',
|
|
1048
|
+
`Kickoff version / نسخهٔ آغاز: ${source.kickoffVersionId}`,
|
|
1049
|
+
'Assumptions stay labeled as assumptions. Validated findings cite a method or source.',
|
|
1050
|
+
'فرضیهها فرضیه میمانند. یافتهٔ اعتبارسنجیشده روش یا منبع دارد.',
|
|
1051
|
+
'',
|
|
1052
|
+
'Pinned introduction / معرفی تأییدشده',
|
|
1053
|
+
source.kickoffPlainText || 'Unknown / نامعلوم',
|
|
1054
|
+
'',
|
|
1055
|
+
`Decision / تصمیم: ${source.decision || 'Unknown / نامعلوم'}`,
|
|
1056
|
+
`Rationale / دلیل: ${source.rationale || 'Unknown / نامعلوم'}`,
|
|
1057
|
+
];
|
|
1058
|
+
for (const finding of source.findings) {
|
|
1059
|
+
const sources = parseSourceRefs(finding.sourceRefs);
|
|
1060
|
+
lines.push('', `${kickoffText(finding.kind) || 'item'} / ${discoveryStance(finding.stance)}`, kickoffText(finding.title) || 'Unknown / نامعلوم', kickoffText(finding.claim) || kickoffText(finding.body) || 'Unknown / نامعلوم', `Method / روش: ${kickoffText(finding.method) || 'Unknown / نامعلوم'}`, `User group / گروه کاربر: ${kickoffText(finding.userGroup) || 'Unknown / نامعلوم'}`, `Date / تاریخ: ${kickoffText(finding.researchDate) || 'Unknown / نامعلوم'}`, `Sources / منابع: ${sources.length ? sources.join('; ') : 'Unknown / نامعلوم'}`, `Kickoff version / نسخه: ${kickoffText(finding.kickoffVersionId) || source.kickoffVersionId}`);
|
|
1061
|
+
}
|
|
1062
|
+
lines.push('', `Fingerprint: ${fingerprint}`);
|
|
1063
|
+
const plainText = lines.join('\n');
|
|
1064
|
+
const html = [
|
|
1065
|
+
'<!DOCTYPE html>',
|
|
1066
|
+
'<html>',
|
|
1067
|
+
'<head><meta charset="utf-8"><title>Discovery Report</title></head>',
|
|
1068
|
+
'<body>',
|
|
1069
|
+
`<pre>${escapeHtml(plainText)}</pre>`,
|
|
1070
|
+
'</body>',
|
|
1071
|
+
'</html>',
|
|
1072
|
+
].join('');
|
|
1073
|
+
return { fingerprint, html, plainText };
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region ----- Exit gate -----
|
|
1077
|
+
function evaluateDiscoveryExit(input) {
|
|
1078
|
+
const stay = (message, discoveryVersionId = '') => ({
|
|
1079
|
+
ok: false,
|
|
1080
|
+
advance: false,
|
|
1081
|
+
message,
|
|
1082
|
+
discoveryVersionId,
|
|
1083
|
+
});
|
|
1084
|
+
const phase = kickoffText(input.currentPhase);
|
|
1085
|
+
const decision = kickoffText(input.decision);
|
|
1086
|
+
const approvedVersionId = kickoffText(input.approvedVersionId);
|
|
1087
|
+
const fingerprint = kickoffText(input.reportFingerprint);
|
|
1088
|
+
const approvedFingerprint = kickoffText(input.approvedFingerprint);
|
|
1089
|
+
const hostVersionId = kickoffText(input.hostVersionId);
|
|
1090
|
+
const statusId = kickoffText(input.statusId);
|
|
1091
|
+
if (phase && phase !== 'discovery' && phase !== 'introduction') {
|
|
1092
|
+
return { ok: true, advance: false, message: '', discoveryVersionId: approvedVersionId };
|
|
1093
|
+
}
|
|
1094
|
+
if (!kickoffText(input.kickoffApprovedVersionId)) {
|
|
1095
|
+
return stay('Discovery needs the approved introduction kickoff version.');
|
|
1096
|
+
}
|
|
1097
|
+
if (decision !== 'proceed' && decision !== 'revise' && decision !== 'stop') {
|
|
1098
|
+
return stay('Discovery needs a decision: proceed, revise, or stop.');
|
|
1099
|
+
}
|
|
1100
|
+
if (!kickoffText(input.rationale)) {
|
|
1101
|
+
return stay('Discovery needs a recorded rationale for the decision.');
|
|
1102
|
+
}
|
|
1103
|
+
if (!approvedVersionId || statusId !== 'approved' || hostVersionId !== approvedVersionId) {
|
|
1104
|
+
return stay('Discovery needs an approved discovery report before the phase can change.');
|
|
1105
|
+
}
|
|
1106
|
+
if (!fingerprint || approvedFingerprint !== fingerprint) {
|
|
1107
|
+
return stay('Discovery findings changed after approval. The approved report was not changed. Generate and approve a new report.', approvedVersionId);
|
|
1108
|
+
}
|
|
1109
|
+
if (decision === 'revise' || decision === 'stop') {
|
|
1110
|
+
return {
|
|
1111
|
+
ok: true,
|
|
1112
|
+
advance: false,
|
|
1113
|
+
message: decision === 'stop'
|
|
1114
|
+
? 'Discovery stops here. The approved report is kept and Scope does not start.'
|
|
1115
|
+
: 'Discovery stays open for revision. The approved report is kept and Scope does not start.',
|
|
1116
|
+
discoveryVersionId: approvedVersionId,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
const findings = input.findings ?? [];
|
|
1120
|
+
if (!findings.some(findingSupportsProceed)) {
|
|
1121
|
+
return stay('Proceed needs at least one validated user or problem finding with a method or source. A questionnaire alone is not a conclusion.', approvedVersionId);
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
ok: true,
|
|
1125
|
+
advance: true,
|
|
1126
|
+
message: '',
|
|
1127
|
+
discoveryVersionId: approvedVersionId,
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
//#endregion
|
|
1131
|
+
|
|
1132
|
+
function textPresent(value) {
|
|
1133
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
1134
|
+
}
|
|
1135
|
+
function canCompleteIntroduction(software) {
|
|
1136
|
+
return missingIntroductionFields(software).length === 0;
|
|
1137
|
+
}
|
|
1138
|
+
function isIntroductionComplete(software) {
|
|
1139
|
+
if (software.introductionCompletedAt) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
return isSoftwarePhaseAtLeast(String(software.currentPhase ?? ''), 'discovery');
|
|
1143
|
+
}
|
|
1144
|
+
function canLinkProject(software) {
|
|
1145
|
+
return isIntroductionComplete(software);
|
|
1146
|
+
}
|
|
1147
|
+
function canWriteDesignKind(software, kind) {
|
|
1148
|
+
if (kind === 'technical') {
|
|
1149
|
+
return isSoftwarePhaseAtLeast(String(software.currentPhase ?? ''), 'design');
|
|
1150
|
+
}
|
|
1151
|
+
return isSoftwarePhaseAtLeast(String(software.currentPhase ?? ''), 'behavior-specification');
|
|
1152
|
+
}
|
|
1153
|
+
const INTRODUCTION_UPSTREAM_FIELDS = [
|
|
1154
|
+
'title',
|
|
1155
|
+
'idea',
|
|
1156
|
+
'initialProblem',
|
|
1157
|
+
'goal',
|
|
1158
|
+
'ownerId',
|
|
1159
|
+
'potentialUsers',
|
|
1160
|
+
'openQuestions',
|
|
1161
|
+
];
|
|
1162
|
+
function canCompletePhase(phase, software, kickoff, discovery) {
|
|
1163
|
+
if (phase === 'discovery') {
|
|
1164
|
+
const decision = evaluateDiscoveryExit({
|
|
1165
|
+
currentPhase: software.currentPhase,
|
|
1166
|
+
...discovery,
|
|
1167
|
+
});
|
|
1168
|
+
return { ok: decision.ok, message: decision.message, advance: decision.advance };
|
|
1169
|
+
}
|
|
1170
|
+
if (phase === 'introduction') {
|
|
1171
|
+
const alreadyLeftIntroduction = isIntroductionComplete(software) && kickoffText(software.currentPhase) !== 'introduction';
|
|
1172
|
+
if (alreadyLeftIntroduction) {
|
|
1173
|
+
return { ok: true, message: '' };
|
|
1174
|
+
}
|
|
1175
|
+
const decision = evaluateIntroductionExit(software, kickoff);
|
|
1176
|
+
return { ok: decision.ok, message: decision.message, missingFields: decision.missingFields };
|
|
1177
|
+
}
|
|
1178
|
+
if (software.currentPhase !== phase) {
|
|
1179
|
+
const next = nextSoftwarePhase(phase);
|
|
1180
|
+
if (next && isSoftwarePhaseAtLeast(String(software.currentPhase ?? ''), next)) {
|
|
1181
|
+
return { ok: true, message: '' };
|
|
1182
|
+
}
|
|
1183
|
+
return {
|
|
1184
|
+
ok: false,
|
|
1185
|
+
message: `Software must be in ${phase} before this phase can complete.`,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
return { ok: true, message: '' };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Generated bundle index. Do not edit.
|
|
1193
|
+
*/
|
|
1194
|
+
|
|
1195
|
+
export { AXPSoftwareManagementCommandKeys, AXPSoftwareManagementMenuKeys, AXPSoftwareManagementPermissionKeys, DISCOVERY_DECISIONS, DISCOVERY_FINDINGS_CHANGED, DISCOVERY_KICKOFF_DOCUMENT_TYPE_NAME, DISCOVERY_KICKOFF_FILE_NAME, DISCOVERY_METHODS, DISCOVERY_REPORT_DOCUMENT_TYPE_NAME, DISCOVERY_REPORT_FILE_NAME, DISCOVERY_STANCES, INTRODUCTION_EXIT_MESSAGE, INTRODUCTION_REQUIRED_FIELDS, INTRODUCTION_UPSTREAM_FIELDS, KICKOFF_REVIEW_REASON_BASE_DATA, KICKOFF_REVIEW_STATUS, KICKOFF_SOURCE_FIELDS, RootConfig, SOFTWARE_DEFINITION_WORKFLOW_NAME, SOFTWARE_DESIGN_KINDS, SOFTWARE_DISCOVERY_KINDS, SOFTWARE_ORIGINS, SOFTWARE_PHASES, SOFTWARE_PROPOSED_BY, SOFTWARE_SCOPE_KINDS, SoftwareManagementManifest, approvedKickoffPdfSource, buildDiscoveryKickoffSheet, buildDiscoveryReport, buildIntroductionKickoffCheck, canCompleteIntroduction, canCompletePhase, canLinkProject, canWriteDesignKind, discoveryKickoffFingerprint, discoveryReportFingerprint, discoveryStance, evaluateDiscoveryExit, evaluateIntroductionExit, findingSupportsProceed, introductionValueText, isIntroductionComplete, isSoftwarePhaseAtLeast, kickoffAfterBaseDataChange, kickoffFingerprintFromRows, kickoffPlainTextFromVersion, kickoffReviewKeepsIntroduction, kickoffSourceFromSoftware, kickoffStatusId, kickoffText, missingIntroductionFields, nextSoftwarePhase, parseSourceRefs, readKickoffMeta, softwareDefinitionWorkflowDefinition, softwarePhaseIndex, textPresent };
|
|
1196
|
+
//# sourceMappingURL=acorex-platform-module-software-management-client-contracts.mjs.map
|