@sppg2001/atomize 4.0.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js
CHANGED
|
@@ -178,6 +178,35 @@ var init_errors = __esm(() => {
|
|
|
178
178
|
};
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// ../atomize-schema/src/estimate-format.ts
|
|
182
|
+
function formatEstimateValue(value) {
|
|
183
|
+
return String(Math.round(value * 100) / 100);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ../atomize-schema/src/estimation-fields.ts
|
|
187
|
+
function matchesFieldPattern(referenceName, patterns = []) {
|
|
188
|
+
return patterns.some((pattern) => pattern.endsWith(".*") ? referenceName.startsWith(pattern.slice(0, -1)) : referenceName === pattern);
|
|
189
|
+
}
|
|
190
|
+
function isPlatformExcluded(field, rules) {
|
|
191
|
+
return field.referenceName.startsWith("System.") || matchesFieldPattern(field.referenceName, rules?.nonEstimateFields);
|
|
192
|
+
}
|
|
193
|
+
function isEstimateSourceField(field, rules) {
|
|
194
|
+
if (field.isReadOnly)
|
|
195
|
+
return false;
|
|
196
|
+
if (rules?.storyEstimateFields?.includes(field.referenceName))
|
|
197
|
+
return true;
|
|
198
|
+
if (isPlatformExcluded(field, rules))
|
|
199
|
+
return false;
|
|
200
|
+
return field.type === "integer" || field.type === "decimal" || field.type === "string" && !field.isMultiline;
|
|
201
|
+
}
|
|
202
|
+
function isEstimateTargetField(field, rules) {
|
|
203
|
+
return !field.isReadOnly && !field.isPicklist && (field.type === "integer" || field.type === "decimal") && !isPlatformExcluded(field, rules);
|
|
204
|
+
}
|
|
205
|
+
function sortEstimateFields(fields) {
|
|
206
|
+
const custom = (field) => Number(field.isCustom ?? field.referenceName.startsWith("Custom."));
|
|
207
|
+
return [...fields].sort((a, b) => custom(b) - custom(a) || a.name.localeCompare(b.name));
|
|
208
|
+
}
|
|
209
|
+
|
|
181
210
|
// ../atomize-schema/src/estimation-normalizer.ts
|
|
182
211
|
function normalizeEstimationPercentages(items, options = {}) {
|
|
183
212
|
const { skipIfAlreadyNormalized = true, tolerance = 0.01, targetTotal = 100 } = options;
|
|
@@ -20065,27 +20094,86 @@ var init_zod = __esm(() => {
|
|
|
20065
20094
|
});
|
|
20066
20095
|
|
|
20067
20096
|
// ../atomize-schema/src/schema.ts
|
|
20097
|
+
function lookupTableEstimate(value, table, multipliers = false) {
|
|
20098
|
+
const text = String(value).trim();
|
|
20099
|
+
if (Object.hasOwn(table, text))
|
|
20100
|
+
return table[text];
|
|
20101
|
+
if (!multipliers)
|
|
20102
|
+
return;
|
|
20103
|
+
const candidates = Object.keys(table).filter((key) => key !== "" && text.endsWith(key)).sort((a, b) => b.length - a.length);
|
|
20104
|
+
for (const key of candidates) {
|
|
20105
|
+
const prefix = text.slice(0, text.length - key.length).trim();
|
|
20106
|
+
if (NUMBER_PATTERN.test(prefix))
|
|
20107
|
+
return Number(prefix) * table[key];
|
|
20108
|
+
}
|
|
20109
|
+
return;
|
|
20110
|
+
}
|
|
20111
|
+
function isConvertibleStoryEstimate(value, conversion) {
|
|
20112
|
+
if (conversion?.table)
|
|
20113
|
+
return lookupTableEstimate(value, conversion.table, conversion.multipliers) !== undefined;
|
|
20114
|
+
return typeof value === "number" || value.trim() !== "" && Number.isFinite(Number(value));
|
|
20115
|
+
}
|
|
20068
20116
|
function ownPercentRange(task) {
|
|
20069
20117
|
const candidates = [task.estimationPercent ?? 0, ...task.estimationPercentCondition?.map((rule) => rule.percent) ?? []];
|
|
20070
20118
|
return [Math.min(...candidates), Math.max(...candidates)];
|
|
20071
20119
|
}
|
|
20120
|
+
function copyCount(task) {
|
|
20121
|
+
return task.repeat ?? 1;
|
|
20122
|
+
}
|
|
20123
|
+
function generatedTaskCount(tasks) {
|
|
20124
|
+
return tasks.reduce((sum, task) => sum + copyCount(task), 0);
|
|
20125
|
+
}
|
|
20126
|
+
function validateRepeatedTasks(tasks, ctx, maxRepeat) {
|
|
20127
|
+
const repeatedIds = new Set(tasks.filter((t) => t.repeat !== undefined && t.id).map((t) => t.id));
|
|
20128
|
+
tasks.forEach((task, index) => {
|
|
20129
|
+
if (maxRepeat !== undefined && task.repeat !== undefined && task.repeat > maxRepeat) {
|
|
20130
|
+
ctx.addIssue({
|
|
20131
|
+
code: "custom",
|
|
20132
|
+
path: ["tasks", index, "repeat"],
|
|
20133
|
+
message: `Task repeats ${task.repeat} times, but the maximum is ${maxRepeat}. Lower repeat or raise validation.maxRepeat.`,
|
|
20134
|
+
params: { code: "REPEAT_EXCEEDS_MAX", maxRepeat }
|
|
20135
|
+
});
|
|
20136
|
+
}
|
|
20137
|
+
if (task.repeat !== undefined && task.dependsOn?.length) {
|
|
20138
|
+
ctx.addIssue({
|
|
20139
|
+
code: "custom",
|
|
20140
|
+
path: ["tasks", index, "dependsOn"],
|
|
20141
|
+
message: "A repeated task cannot have dependsOn. Remove repeat or dependsOn from this task.",
|
|
20142
|
+
params: { code: "REPEAT_WITH_DEPENDENCY" }
|
|
20143
|
+
});
|
|
20144
|
+
}
|
|
20145
|
+
task.dependsOn?.forEach((depId) => {
|
|
20146
|
+
if (repeatedIds.has(depId)) {
|
|
20147
|
+
ctx.addIssue({
|
|
20148
|
+
code: "custom",
|
|
20149
|
+
path: ["tasks", index, "dependsOn"],
|
|
20150
|
+
message: `Task depends on "${depId}", which is repeated. A task cannot depend on a repeated task.`,
|
|
20151
|
+
params: { code: "DEPENDS_ON_REPEATED_TASK", taskId: depId }
|
|
20152
|
+
});
|
|
20153
|
+
}
|
|
20154
|
+
});
|
|
20155
|
+
});
|
|
20156
|
+
}
|
|
20072
20157
|
function validateTaskConstraints(v, tasks, ctx) {
|
|
20073
|
-
|
|
20074
|
-
|
|
20158
|
+
const count = generatedTaskCount(tasks);
|
|
20159
|
+
const hasRepeats = tasks.some((task) => copyCount(task) > 1);
|
|
20160
|
+
const counted = hasRepeats ? " (counting each repeated copy)" : "";
|
|
20161
|
+
if (v?.minTasks !== undefined && count < v.minTasks) {
|
|
20162
|
+
const needed = v.minTasks - count;
|
|
20075
20163
|
ctx.addIssue({
|
|
20076
20164
|
code: "custom",
|
|
20077
20165
|
path: ["tasks"],
|
|
20078
|
-
message: `Template has ${
|
|
20079
|
-
params: { code: "TOO_FEW_TASKS" }
|
|
20166
|
+
message: `Template has ${count} task(s)${counted}, but minimum is ${v.minTasks}. Add ${needed} more task(s).`,
|
|
20167
|
+
params: { code: "TOO_FEW_TASKS", count, limit: v.minTasks, hasRepeats }
|
|
20080
20168
|
});
|
|
20081
20169
|
}
|
|
20082
|
-
if (v?.maxTasks !== undefined &&
|
|
20083
|
-
const excess =
|
|
20170
|
+
if (v?.maxTasks !== undefined && count > v.maxTasks) {
|
|
20171
|
+
const excess = count - v.maxTasks;
|
|
20084
20172
|
ctx.addIssue({
|
|
20085
20173
|
code: "custom",
|
|
20086
20174
|
path: ["tasks"],
|
|
20087
|
-
message: `Template has ${
|
|
20088
|
-
params: { code: "TOO_MANY_TASKS" }
|
|
20175
|
+
message: `Template has ${count} task(s)${counted}, but maximum is ${v.maxTasks}. Remove ${excess} task(s) or increase maxTasks.`,
|
|
20176
|
+
params: { code: "TOO_MANY_TASKS", count, limit: v.maxTasks, hasRepeats }
|
|
20089
20177
|
});
|
|
20090
20178
|
}
|
|
20091
20179
|
}
|
|
@@ -20107,14 +20195,14 @@ function validateUniqueTaskIds(tasks, ctx) {
|
|
|
20107
20195
|
});
|
|
20108
20196
|
});
|
|
20109
20197
|
}
|
|
20110
|
-
function validateEstimationConstraints(v, totalPercent, maxTotalPercent, ctx) {
|
|
20198
|
+
function validateEstimationConstraints(v, totalPercent, maxTotalPercent, hasRepeats, ctx) {
|
|
20111
20199
|
if (v?.totalEstimationMustBe !== undefined) {
|
|
20112
20200
|
if (v.totalEstimationMustBe < totalPercent || v.totalEstimationMustBe > maxTotalPercent) {
|
|
20113
20201
|
ctx.addIssue({
|
|
20114
20202
|
code: "custom",
|
|
20115
20203
|
path: ["tasks"],
|
|
20116
20204
|
message: `Total estimation is ${totalPercent}%, but must be ${v.totalEstimationMustBe}%.`,
|
|
20117
|
-
params: { code: "INVALID_TOTAL_ESTIMATION" }
|
|
20205
|
+
params: { code: "INVALID_TOTAL_ESTIMATION", hasRepeats }
|
|
20118
20206
|
});
|
|
20119
20207
|
}
|
|
20120
20208
|
} else if (v?.totalEstimationRange) {
|
|
@@ -20124,7 +20212,7 @@ function validateEstimationConstraints(v, totalPercent, maxTotalPercent, ctx) {
|
|
|
20124
20212
|
code: "custom",
|
|
20125
20213
|
path: ["tasks"],
|
|
20126
20214
|
message: `Total estimation is ${totalPercent}%, but must be between ${min}% and ${max}%.`,
|
|
20127
|
-
params: { code: "INVALID_ESTIMATION_RANGE" }
|
|
20215
|
+
params: { code: "INVALID_ESTIMATION_RANGE", hasRepeats }
|
|
20128
20216
|
});
|
|
20129
20217
|
}
|
|
20130
20218
|
}
|
|
@@ -20162,7 +20250,7 @@ function reportCircularDependencies(tasks, taskIndexById, ctx) {
|
|
|
20162
20250
|
});
|
|
20163
20251
|
}
|
|
20164
20252
|
}
|
|
20165
|
-
var DATE_OR_MACRO_RE, DateOrMacroSchema, REFERENCE_NAME_RE, ConditionOperatorSchema, ConditionValueSchema, MULTI_VALUE_STANDARD_FIELDS, MULTI_VALUE_FIELD_OPERATORS, ConditionSchema, SavedQuerySchema, FilterCriteriaSchema, EstimationPercentConditionSchema, TaskDefinitionSchema, EstimationConfigSchema, ValidationModeSchema, ValidationConfigSchema, MetadataSchema, TaskTemplateBaseSchema, ExtendingTaskTemplateSchema, TaskTemplateSchema, MixinTemplateSchema, CURRENT_ITERATION = "@CurrentIteration", TEAM_AREAS = "@TeamAreas";
|
|
20253
|
+
var DATE_OR_MACRO_RE, DateOrMacroSchema, REFERENCE_NAME_RE, ConditionOperatorSchema, ConditionValueSchema, MULTI_VALUE_STANDARD_FIELDS, MULTI_VALUE_FIELD_OPERATORS, ConditionSchema, SavedQuerySchema, FilterCriteriaSchema, EstimationPercentConditionSchema, TaskDefinitionSchema, NUMBER_PATTERN, EstimationConversionSchema, EstimationConfigSchema, ValidationModeSchema, ValidationConfigSchema, MetadataSchema, TaskTemplateBaseSchema, ExtendingTaskTemplateSchema, TaskTemplateSchema, DEFAULT_MAX_REPEAT = 20, MixinTemplateSchema, CURRENT_ITERATION = "@CurrentIteration", TEAM_AREAS = "@TeamAreas";
|
|
20166
20254
|
var init_schema = __esm(() => {
|
|
20167
20255
|
init_zod();
|
|
20168
20256
|
DATE_OR_MACRO_RE = /^(@Today|@StartOfDay|@StartOfMonth|@StartOfWeek|@StartOfYear)(\s*[+-]\s*\d+)?$|^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/i;
|
|
@@ -20219,7 +20307,7 @@ var init_schema = __esm(() => {
|
|
|
20219
20307
|
});
|
|
20220
20308
|
FilterCriteriaSchema = exports_external.object({
|
|
20221
20309
|
team: exports_external.string().describe("Azure DevOps team name. Limits story selection to stories owned by this team.").optional(),
|
|
20222
|
-
workItemTypes: exports_external.array(exports_external.string()).describe("Work item types to include (e.g. ['User Story', 'Bug']).").optional(),
|
|
20310
|
+
workItemTypes: exports_external.array(exports_external.string()).describe("Work item types to include (e.g. ['User Story', 'Bug']). With savedQuery, declares which types the query returns: it doesn't filter, but tells Atomize which fields to look up and validate.").optional(),
|
|
20223
20311
|
states: exports_external.array(exports_external.string()).describe("Story states to target (e.g. ['Active', 'New']).").optional(),
|
|
20224
20312
|
statesExclude: exports_external.array(exports_external.string()).describe("Story states to exclude from selection.").optional(),
|
|
20225
20313
|
statesWereEver: exports_external.array(exports_external.string()).describe("Include stories that were ever in any of these states.").optional(),
|
|
@@ -20256,6 +20344,7 @@ var init_schema = __esm(() => {
|
|
|
20256
20344
|
tags: exports_external.array(exports_external.string()).describe("Tags applied to the generated task.").optional(),
|
|
20257
20345
|
condition: ConditionSchema.describe("Condition evaluated against the parent story. If it does not match, this task is skipped.").optional(),
|
|
20258
20346
|
dependsOn: exports_external.array(exports_external.string()).describe("IDs of tasks this task depends on. Atomize creates predecessor dependency links between these tasks.").optional(),
|
|
20347
|
+
repeat: exports_external.number().int("Repeat count must be a whole number").min(1, "Repeat count must be at least 1").describe("Number of identical Tasks to generate from this definition (default 1). Each copy's title gets its position, e.g. 'Review (2)'. Cannot be combined with dependsOn.").optional(),
|
|
20259
20348
|
assignTo: exports_external.string().describe("Email address or alias to assign this task to.").optional(),
|
|
20260
20349
|
priority: exports_external.number().describe("Priority value for the generated task.").optional(),
|
|
20261
20350
|
activity: exports_external.string().describe("Azure DevOps activity type for this task (e.g. 'Development', 'Testing', 'Design').").optional(),
|
|
@@ -20276,14 +20365,42 @@ var init_schema = __esm(() => {
|
|
|
20276
20365
|
return;
|
|
20277
20366
|
})
|
|
20278
20367
|
}).strict();
|
|
20368
|
+
NUMBER_PATTERN = /^(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
20369
|
+
EstimationConversionSchema = exports_external.object({
|
|
20370
|
+
factor: exports_external.number().positive().describe("Multiplier from the Story's unit to the Task's unit, e.g. 4 when 1 point = 4 hours.").optional(),
|
|
20371
|
+
table: exports_external.record(exports_external.string(), exports_external.number().nonnegative()).refine((table) => Object.keys(table).length > 0, "conversion.table must have at least one entry").describe("Story Estimate value to Task-unit amount, matched exactly, e.g. { XS: 1, S: 2, M: 4, L: 5 } or { 1: 2, 2: 4, 3: 8 } for scales that aren't linear.").optional(),
|
|
20372
|
+
multipliers: exports_external.boolean().describe("Also accept a number followed by a table key, e.g. '0.3XL' = 0.3 × XL. Exact keys still win. Requires a table with non-numeric keys.").optional()
|
|
20373
|
+
}).strict().refine((conversion) => conversion.factor === undefined !== (conversion.table === undefined), {
|
|
20374
|
+
message: "conversion must set exactly one of factor or table"
|
|
20375
|
+
}).refine((conversion) => !conversion.multipliers || conversion.table !== undefined, {
|
|
20376
|
+
message: "conversion.multipliers requires conversion.table",
|
|
20377
|
+
path: ["multipliers"]
|
|
20378
|
+
}).refine((conversion) => !conversion.multipliers || !conversion.table || !Object.keys(conversion.table).every((key) => NUMBER_PATTERN.test(key)), {
|
|
20379
|
+
message: "conversion.multipliers cannot be used when every table key is numeric: '25' would be ambiguous between the key 25 and 2 × 5",
|
|
20380
|
+
path: ["multipliers"]
|
|
20381
|
+
}).describe("Translates the Story Estimate into the Task's unit before it is split across tasks. Set exactly one of factor or table; omit for one-to-one.");
|
|
20279
20382
|
EstimationConfigSchema = exports_external.object({
|
|
20280
20383
|
strategy: exports_external.enum(["percentage"]).optional().default("percentage").describe("Estimation strategy. 'percentage' distributes the parent story's estimate across tasks proportionally."),
|
|
20281
|
-
source: exports_external.string().describe("
|
|
20384
|
+
source: exports_external.string().min(1).describe("Story field that supplies the Story Estimate, as a platform field reference name (e.g. 'Custom.TShirtSize'). Replaces the platform's default fields, with no fallback. Omit to use the platform default.").optional(),
|
|
20385
|
+
conversion: EstimationConversionSchema.optional(),
|
|
20386
|
+
targetFields: exports_external.array(exports_external.string().min(1)).min(1, "targetFields must name at least one field").describe("Task fields that receive the Task Estimate, as platform field reference names. Replaces the platform's default estimate fields entirely; values are written exactly as calculated.").optional(),
|
|
20387
|
+
normalize: exports_external.enum(["auto", "always", "never"]).describe("What to do when the generated tasks' percentages don't add up to 100%. 'auto' (default) scales totals below 100% up and leaves totals above 100% as written unless you choose otherwise at generate time; 'always' scales to 100% in both directions; 'never' uses the percentages exactly as written, so a skipped task's share stays unallocated.").optional(),
|
|
20282
20388
|
rounding: exports_external.enum(["nearest", "up", "down", "none"]).optional().default("none").describe("Rounding mode applied to calculated task estimates: 'nearest', 'up', 'down', or 'none'."),
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
|
|
20389
|
+
minimumTaskEstimate: exports_external.number().describe("Minimum Task Estimate any single task can receive after rounding, in the Task's unit.").optional(),
|
|
20390
|
+
minimumTaskPoints: exports_external.number().describe("Deprecated: use minimumTaskEstimate. Still honoured when minimumTaskEstimate is absent.").meta({ deprecated: true }).optional(),
|
|
20391
|
+
ifParentHasNoEstimation: exports_external.enum(["skip", "warn", "use-default"]).describe("Behaviour when the Story Estimate is missing or cannot be converted. 'warn' (default) creates the tasks with blank estimates and reports a warning, 'skip' creates no tasks for the Story, 'use-default' uses defaultParentEstimation instead.").optional(),
|
|
20392
|
+
defaultParentEstimation: exports_external.union([exports_external.number(), exports_external.string().min(1)]).describe("Story Estimate used when the real one is missing or cannot be converted and ifParentHasNoEstimation is 'use-default', in the Story's unit (e.g. 'M' or 5). Goes through the same conversion as a real Story Estimate.").optional()
|
|
20393
|
+
}).strict().superRefine((config2, ctx) => {
|
|
20394
|
+
if (config2.ifParentHasNoEstimation !== "use-default" || config2.defaultParentEstimation === undefined)
|
|
20395
|
+
return;
|
|
20396
|
+
if (isConvertibleStoryEstimate(config2.defaultParentEstimation, config2.conversion))
|
|
20397
|
+
return;
|
|
20398
|
+
ctx.addIssue({
|
|
20399
|
+
code: "custom",
|
|
20400
|
+
path: ["defaultParentEstimation"],
|
|
20401
|
+
message: config2.conversion?.table ? `defaultParentEstimation "${config2.defaultParentEstimation}" is not a key of conversion.table${config2.conversion.multipliers ? " or a multiple of one" : ""}` : `defaultParentEstimation "${config2.defaultParentEstimation}" is not numeric and no conversion.table is set`
|
|
20402
|
+
});
|
|
20403
|
+
});
|
|
20287
20404
|
ValidationModeSchema = exports_external.enum(["strict", "lenient"]).describe("'strict' fails on any validation warning; 'lenient' allows warnings to pass.");
|
|
20288
20405
|
ValidationConfigSchema = exports_external.object({
|
|
20289
20406
|
mode: ValidationModeSchema.describe("'strict' fails on any validation warning; 'lenient' allows warnings to pass.").optional(),
|
|
@@ -20294,6 +20411,7 @@ var init_schema = __esm(() => {
|
|
|
20294
20411
|
}).strict().describe("Sum of all non-conditional task percentages must fall within this range; bounds may exceed 100 when tasks span roles.").optional(),
|
|
20295
20412
|
minTasks: exports_external.number().describe("Minimum number of tasks the template must define.").optional(),
|
|
20296
20413
|
maxTasks: exports_external.number().describe("Maximum number of tasks the template may define.").optional(),
|
|
20414
|
+
maxRepeat: exports_external.number().int("Maximum repeat count must be a whole number").min(1, "Maximum repeat count must be at least 1").describe("Highest 'repeat' any task in this template may use (default 20). Raise it for a template that genuinely needs more copies.").optional(),
|
|
20297
20415
|
taskEstimationRange: exports_external.object({
|
|
20298
20416
|
min: exports_external.number().describe("Minimum allowed estimation percentage for a single task."),
|
|
20299
20417
|
max: exports_external.number().describe("Maximum allowed estimation percentage for a single task.")
|
|
@@ -20326,6 +20444,7 @@ var init_schema = __esm(() => {
|
|
|
20326
20444
|
lastModified: exports_external.string().describe("ISO 8601 date of the last modification.").optional(),
|
|
20327
20445
|
filter: FilterCriteriaSchema.describe("Criteria used to select which stories this template applies to."),
|
|
20328
20446
|
tasks: exports_external.array(TaskDefinitionSchema).min(1, "At least one task is required").describe("Task definitions that will be generated from this template."),
|
|
20447
|
+
taskType: exports_external.string().min(1).describe("Work Item type generated Tasks are created as, in the platform's own vocabulary (e.g. 'Task', 'Sub-task'). Omit to use the platform default.").optional(),
|
|
20329
20448
|
estimation: EstimationConfigSchema.describe("Global estimation configuration for all tasks in this template.").optional(),
|
|
20330
20449
|
validation: ValidationConfigSchema.describe("Validation rules applied when this template is used.").optional(),
|
|
20331
20450
|
metadata: MetadataSchema.describe("Additional metadata for template discovery and documentation.").optional(),
|
|
@@ -20347,12 +20466,13 @@ var init_schema = __esm(() => {
|
|
|
20347
20466
|
let maxTotalPercent = 0;
|
|
20348
20467
|
for (const task of tasks) {
|
|
20349
20468
|
const [ownMin, ownMax] = ownPercentRange(task);
|
|
20350
|
-
|
|
20351
|
-
|
|
20469
|
+
const copies = copyCount(task);
|
|
20470
|
+
totalPercent += task.condition ? 0 : ownMin * copies;
|
|
20471
|
+
maxTotalPercent += ownMax * copies;
|
|
20352
20472
|
}
|
|
20353
20473
|
const taskIds = new Set(tasks.map((t) => t.id).filter(Boolean));
|
|
20354
20474
|
validateUniqueTaskIds(tasks, ctx);
|
|
20355
|
-
validateEstimationConstraints(v, totalPercent, maxTotalPercent, ctx);
|
|
20475
|
+
validateEstimationConstraints(v, totalPercent, maxTotalPercent, tasks.some((t) => copyCount(t) > 1), ctx);
|
|
20356
20476
|
validateTaskConstraints(v, tasks, ctx);
|
|
20357
20477
|
validateRequiredTasks(v, tasks, ctx);
|
|
20358
20478
|
const availableIds = Array.from(taskIds);
|
|
@@ -20361,6 +20481,7 @@ var init_schema = __esm(() => {
|
|
|
20361
20481
|
if (t.id)
|
|
20362
20482
|
taskIndexById.set(t.id, i);
|
|
20363
20483
|
});
|
|
20484
|
+
validateRepeatedTasks(tasks, ctx, v?.maxRepeat ?? DEFAULT_MAX_REPEAT);
|
|
20364
20485
|
tasks.forEach((task, index) => {
|
|
20365
20486
|
task.dependsOn?.forEach((depId) => {
|
|
20366
20487
|
if (!taskIds.has(depId)) {
|
|
@@ -20383,6 +20504,7 @@ var init_schema = __esm(() => {
|
|
|
20383
20504
|
origin: exports_external.string().describe("Catalog ref (template:<name> or mixin:<name>) this mixin was derived from. Informational only — does not affect ref resolution.").optional()
|
|
20384
20505
|
}).strict().superRefine((data, ctx) => {
|
|
20385
20506
|
validateUniqueTaskIds(data.tasks, ctx);
|
|
20507
|
+
validateRepeatedTasks(data.tasks, ctx);
|
|
20386
20508
|
});
|
|
20387
20509
|
});
|
|
20388
20510
|
|
|
@@ -20933,6 +21055,34 @@ var init_story_batch_processor = __esm(() => {
|
|
|
20933
21055
|
init_errors();
|
|
20934
21056
|
});
|
|
20935
21057
|
|
|
21058
|
+
// ../atomize-core/src/core/estimation-field-mapping.ts
|
|
21059
|
+
function resolveEstimationFieldMapping(defaults, overrides = {}) {
|
|
21060
|
+
const storySource = overrides.source ? { kind: "override", field: overrides.source } : { kind: "default", fields: defaults.storyEstimateFields };
|
|
21061
|
+
const taskTarget = overrides.targetFields?.length ? { kind: "override", fields: [...overrides.targetFields] } : { kind: "default", fields: defaults.taskEstimateFields };
|
|
21062
|
+
return {
|
|
21063
|
+
storySource,
|
|
21064
|
+
taskWorkItemType: overrides.taskType ?? defaults.taskWorkItemType,
|
|
21065
|
+
taskTarget,
|
|
21066
|
+
unitLabel: taskTarget.kind === "default" ? defaults.unitLabel : undefined
|
|
21067
|
+
};
|
|
21068
|
+
}
|
|
21069
|
+
function readStoryEstimate(story, source) {
|
|
21070
|
+
const value = source.kind === "default" ? story.estimation : story.customFields?.[source.field];
|
|
21071
|
+
return isStoryEstimate(value) ? value : undefined;
|
|
21072
|
+
}
|
|
21073
|
+
function numericStoryEstimate(value) {
|
|
21074
|
+
if (typeof value === "number")
|
|
21075
|
+
return Number.isFinite(value) ? value : undefined;
|
|
21076
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
21077
|
+
const parsed = Number(value);
|
|
21078
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
21079
|
+
}
|
|
21080
|
+
return;
|
|
21081
|
+
}
|
|
21082
|
+
function isStoryEstimate(value) {
|
|
21083
|
+
return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim() !== "";
|
|
21084
|
+
}
|
|
21085
|
+
|
|
20936
21086
|
// ../atomize-core/src/core/condition-evaluator.ts
|
|
20937
21087
|
function extractStandardFieldRefs(condition) {
|
|
20938
21088
|
const refs = [];
|
|
@@ -20966,22 +21116,29 @@ function extractCustomFieldRefs(condition) {
|
|
|
20966
21116
|
}
|
|
20967
21117
|
|
|
20968
21118
|
class ConditionEvaluator {
|
|
20969
|
-
evaluateCondition(condition, story) {
|
|
21119
|
+
evaluateCondition(condition, story, storyEstimateSource) {
|
|
20970
21120
|
if (!condition)
|
|
20971
21121
|
return true;
|
|
20972
|
-
return this.evaluate(condition, story);
|
|
21122
|
+
return this.evaluate(condition, story, storyEstimateSource);
|
|
20973
21123
|
}
|
|
20974
|
-
evaluate(condition, story) {
|
|
21124
|
+
evaluate(condition, story, source) {
|
|
20975
21125
|
if ("all" in condition) {
|
|
20976
|
-
return condition.all.every((c2) => this.evaluate(c2, story));
|
|
21126
|
+
return condition.all.every((c2) => this.evaluate(c2, story, source));
|
|
20977
21127
|
}
|
|
20978
21128
|
if ("any" in condition) {
|
|
20979
|
-
return condition.any.some((c2) => this.evaluate(c2, story));
|
|
21129
|
+
return condition.any.some((c2) => this.evaluate(c2, story, source));
|
|
20980
21130
|
}
|
|
20981
21131
|
if ("customField" in condition) {
|
|
20982
21132
|
const fieldValue2 = story.customFields?.[condition.customField];
|
|
20983
21133
|
return this.applyOperator(fieldValue2, condition.operator, condition.value);
|
|
20984
21134
|
}
|
|
21135
|
+
if (condition.field === "estimation") {
|
|
21136
|
+
const estimate = readStoryEstimate(story, source ?? { kind: "default", fields: [] });
|
|
21137
|
+
if (estimate !== undefined && NUMERIC_OPERATORS.has(condition.operator) && numericStoryEstimate(estimate) === undefined) {
|
|
21138
|
+
throw new Error(`cannot compare the Story Estimate "${estimate}" with "${condition.operator}" because it is not numeric; use equals or not-equals for categories`);
|
|
21139
|
+
}
|
|
21140
|
+
return this.applyOperator(estimate, condition.operator, condition.value);
|
|
21141
|
+
}
|
|
20985
21142
|
const fieldValue = this.resolveField(condition.field, story);
|
|
20986
21143
|
return this.applyOperator(fieldValue, condition.operator, condition.value);
|
|
20987
21144
|
}
|
|
@@ -21052,8 +21209,69 @@ class ConditionEvaluator {
|
|
|
21052
21209
|
return compare(a2, b2);
|
|
21053
21210
|
}
|
|
21054
21211
|
}
|
|
21212
|
+
var NUMERIC_OPERATORS;
|
|
21055
21213
|
var init_condition_evaluator = __esm(() => {
|
|
21056
21214
|
init_logger();
|
|
21215
|
+
NUMERIC_OPERATORS = new Set(["gt", "lt", "gte", "lte"]);
|
|
21216
|
+
});
|
|
21217
|
+
|
|
21218
|
+
// ../atomize-core/src/templates/schema.ts
|
|
21219
|
+
var init_schema2 = __esm(() => {
|
|
21220
|
+
init_src();
|
|
21221
|
+
});
|
|
21222
|
+
|
|
21223
|
+
// ../atomize-core/src/core/estimation-conversion.ts
|
|
21224
|
+
function convertStoryEstimate(raw, conversion) {
|
|
21225
|
+
if (raw === undefined) {
|
|
21226
|
+
return { kind: "unresolvable", reason: "Story has no estimate" };
|
|
21227
|
+
}
|
|
21228
|
+
if (conversion?.table) {
|
|
21229
|
+
const total = lookupTableEstimate(raw, conversion.table, conversion.multipliers);
|
|
21230
|
+
if (total === undefined) {
|
|
21231
|
+
const suffix = conversion.multipliers ? " and is not a multiple of a table key" : "";
|
|
21232
|
+
return { kind: "unresolvable", reason: `Story Estimate "${String(raw).trim()}" is not in the conversion table${suffix}` };
|
|
21233
|
+
}
|
|
21234
|
+
return { kind: "resolved", total: roundAwayFloatNoise(total) };
|
|
21235
|
+
}
|
|
21236
|
+
const numeric = numericStoryEstimate(raw);
|
|
21237
|
+
if (numeric === undefined) {
|
|
21238
|
+
return {
|
|
21239
|
+
kind: "unresolvable",
|
|
21240
|
+
reason: `Story Estimate "${raw}" is not numeric and the Template declares no conversion for it`
|
|
21241
|
+
};
|
|
21242
|
+
}
|
|
21243
|
+
const factor = conversion?.factor ?? 1;
|
|
21244
|
+
return { kind: "resolved", total: roundAwayFloatNoise(numeric * factor) };
|
|
21245
|
+
}
|
|
21246
|
+
function roundAwayFloatNoise(value) {
|
|
21247
|
+
return Math.round(value * 1e9) / 1e9;
|
|
21248
|
+
}
|
|
21249
|
+
function resolveStoryEstimate(raw, config2) {
|
|
21250
|
+
const converted = convertStoryEstimate(raw, config2?.conversion);
|
|
21251
|
+
if (converted.kind === "resolved")
|
|
21252
|
+
return converted;
|
|
21253
|
+
switch (config2?.ifParentHasNoEstimation ?? "warn") {
|
|
21254
|
+
case "skip":
|
|
21255
|
+
return { kind: "skip", reason: converted.reason };
|
|
21256
|
+
case "use-default": {
|
|
21257
|
+
if (config2?.defaultParentEstimation === undefined) {
|
|
21258
|
+
return {
|
|
21259
|
+
kind: "blank",
|
|
21260
|
+
reason: `${converted.reason}, and no defaultParentEstimation is set`
|
|
21261
|
+
};
|
|
21262
|
+
}
|
|
21263
|
+
const fallback = convertStoryEstimate(config2.defaultParentEstimation, config2.conversion);
|
|
21264
|
+
if (fallback.kind === "resolved") {
|
|
21265
|
+
return { kind: "resolved", total: fallback.total, fallbackReason: converted.reason };
|
|
21266
|
+
}
|
|
21267
|
+
return { kind: "blank", reason: `${converted.reason}, and the default Story Estimate is unusable: ${fallback.reason}` };
|
|
21268
|
+
}
|
|
21269
|
+
default:
|
|
21270
|
+
return { kind: "blank", reason: converted.reason };
|
|
21271
|
+
}
|
|
21272
|
+
}
|
|
21273
|
+
var init_estimation_conversion = __esm(() => {
|
|
21274
|
+
init_schema2();
|
|
21057
21275
|
});
|
|
21058
21276
|
|
|
21059
21277
|
// ../atomize-core/src/utils/estimation-normalizer.ts
|
|
@@ -21075,7 +21293,9 @@ var init_estimation_normalizer = __esm(() => {
|
|
|
21075
21293
|
// ../atomize-core/src/core/estimation-distribution.ts
|
|
21076
21294
|
function distributeActiveTaskPercentages(tasks, options = {}) {
|
|
21077
21295
|
const totalBefore = totalEstimationPercent(tasks);
|
|
21078
|
-
const
|
|
21296
|
+
const mode = options.mode ?? "auto";
|
|
21297
|
+
const shouldNormalize = mode === "always" ? totalBefore !== 100 : mode === "never" ? false : totalBefore < 100 || totalBefore > 100 && options.forceNormalize === true;
|
|
21298
|
+
const normalized = shouldNormalize ? normalizeEstimationPercentages2(tasks, {
|
|
21079
21299
|
skipIfAlreadyNormalized: false,
|
|
21080
21300
|
enableLogging: options.enableLogging ?? true
|
|
21081
21301
|
}) : false;
|
|
@@ -21100,16 +21320,32 @@ function totalEstimationPercent(tasks) {
|
|
|
21100
21320
|
return tasks.reduce((sum, task) => sum + (task.estimationPercent ?? 0), 0);
|
|
21101
21321
|
}
|
|
21102
21322
|
function totalUnconditionalEstimationPercent(tasks) {
|
|
21103
|
-
return tasks.filter((task) => !task.condition).reduce((sum, task) => sum + (task.estimationPercent ?? 0), 0);
|
|
21323
|
+
return tasks.filter((task) => !task.condition).reduce((sum, task) => sum + (task.estimationPercent ?? 0) * copyCount(task), 0);
|
|
21104
21324
|
}
|
|
21105
21325
|
function shouldOfferOverageNormalization(tasks) {
|
|
21106
21326
|
const total = totalEstimationPercent(tasks);
|
|
21107
21327
|
return { shouldOffer: total > 100, total };
|
|
21108
21328
|
}
|
|
21109
21329
|
var init_estimation_distribution = __esm(() => {
|
|
21330
|
+
init_schema2();
|
|
21110
21331
|
init_estimation_normalizer();
|
|
21111
21332
|
});
|
|
21112
21333
|
|
|
21334
|
+
// ../atomize-core/src/core/task-repeater.ts
|
|
21335
|
+
function repeatTasks(tasks) {
|
|
21336
|
+
return tasks.flatMap((task) => {
|
|
21337
|
+
const iterationCount = task.repeat ?? 1;
|
|
21338
|
+
return Array.from({ length: iterationCount }, (_2, index) => ({
|
|
21339
|
+
task,
|
|
21340
|
+
iteration: index + 1,
|
|
21341
|
+
iterationCount
|
|
21342
|
+
}));
|
|
21343
|
+
});
|
|
21344
|
+
}
|
|
21345
|
+
function withOrdinal(title, { iteration, iterationCount }) {
|
|
21346
|
+
return iterationCount > 1 ? `${title} (${iteration})` : title;
|
|
21347
|
+
}
|
|
21348
|
+
|
|
21113
21349
|
// ../atomize-core/src/core/template-interpolator.ts
|
|
21114
21350
|
function interpolateValue(template, story, onMissing) {
|
|
21115
21351
|
return template.replace(/\${story\.title}/g, story.title).replace(/\${story\.id}/g, story.id).replace(/\${story\.description}/g, story.description ?? "").replace(/\{\{\s*story\.customFields\['([^']+)'\]\s*\}\}/g, (_match, referenceName) => {
|
|
@@ -21130,20 +21366,29 @@ class EstimationCalculator {
|
|
|
21130
21366
|
constructor(conditionEvaluator = new ConditionEvaluator) {
|
|
21131
21367
|
this.conditionEvaluator = conditionEvaluator;
|
|
21132
21368
|
}
|
|
21133
|
-
calculateTasksWithSkipped(story, connectUserEmail, templateTasks, estimationConfig, forceNormalize = false) {
|
|
21369
|
+
calculateTasksWithSkipped(story, connectUserEmail, templateTasks, estimationConfig, forceNormalize = false, fieldMapping) {
|
|
21134
21370
|
logger.debug(`EstimationCalculator: Calculating tasks for story ${story.id}`);
|
|
21135
|
-
const
|
|
21136
|
-
if (
|
|
21137
|
-
logger.warn(`Story ${story.id}
|
|
21371
|
+
const resolution = this.resolveParentEstimate(story, estimationConfig, fieldMapping);
|
|
21372
|
+
if (resolution.kind === "skip") {
|
|
21373
|
+
logger.warn(`Story ${story.id}: ${resolution.reason}. Skipping the Story.`);
|
|
21374
|
+
return {
|
|
21375
|
+
calculatedTasks: [],
|
|
21376
|
+
skippedTasks: [],
|
|
21377
|
+
unresolvedEstimate: { action: "skip", reason: resolution.reason }
|
|
21378
|
+
};
|
|
21379
|
+
}
|
|
21380
|
+
if (resolution.kind === "blank") {
|
|
21381
|
+
logger.warn(`Story ${story.id}: ${resolution.reason}. Task Estimates will be left blank.`);
|
|
21138
21382
|
}
|
|
21139
|
-
const calculatedTasks = [];
|
|
21140
21383
|
const skippedTasks = [];
|
|
21384
|
+
const activeTasks = [];
|
|
21385
|
+
const resolvedPercents = new Map;
|
|
21141
21386
|
for (const templateTask of templateTasks) {
|
|
21142
21387
|
logger.debug(`Processing template task: ${templateTask.title} (Condition: ${templateTask.condition ? JSON.stringify(templateTask.condition) : "none"})`);
|
|
21143
21388
|
if (templateTask.condition) {
|
|
21144
21389
|
let conditionMet;
|
|
21145
21390
|
try {
|
|
21146
|
-
conditionMet = this.conditionEvaluator.evaluateCondition(templateTask.condition, story);
|
|
21391
|
+
conditionMet = this.conditionEvaluator.evaluateCondition(templateTask.condition, story, fieldMapping?.storySource);
|
|
21147
21392
|
} catch (error62) {
|
|
21148
21393
|
const reason = `Condition evaluation error: ${getErrorMessage(error62)}`;
|
|
21149
21394
|
logger.error(`Skipping task "${templateTask.title}" - ${reason}`);
|
|
@@ -21158,25 +21403,52 @@ class EstimationCalculator {
|
|
|
21158
21403
|
}
|
|
21159
21404
|
logger.debug(`Task "${templateTask.title}" - condition met: ${JSON.stringify(templateTask.condition)}`);
|
|
21160
21405
|
}
|
|
21161
|
-
|
|
21162
|
-
|
|
21163
|
-
|
|
21406
|
+
let resolvedPercent;
|
|
21407
|
+
try {
|
|
21408
|
+
resolvedPercent = this.resolveEffectivePercent(templateTask, story, fieldMapping);
|
|
21409
|
+
} catch (error62) {
|
|
21410
|
+
const reason = `Condition evaluation error: ${getErrorMessage(error62)}`;
|
|
21411
|
+
logger.error(`Skipping task "${templateTask.title}" - ${reason}`);
|
|
21412
|
+
skippedTasks.push({ templateTask, reason });
|
|
21413
|
+
continue;
|
|
21414
|
+
}
|
|
21415
|
+
activeTasks.push(templateTask);
|
|
21416
|
+
resolvedPercents.set(templateTask, resolvedPercent);
|
|
21417
|
+
}
|
|
21418
|
+
const calculatedTasks = repeatTasks(activeTasks).map((copy) => {
|
|
21419
|
+
const calculatedTask = this.buildCalculatedTask(copy.task, story, connectUserEmail, 0, resolvedPercents.get(copy.task));
|
|
21420
|
+
calculatedTask.title = withOrdinal(calculatedTask.title, copy);
|
|
21421
|
+
return calculatedTask;
|
|
21422
|
+
});
|
|
21423
|
+
if (fieldMapping) {
|
|
21424
|
+
for (const task of calculatedTasks) {
|
|
21425
|
+
task.workItemType = fieldMapping.taskWorkItemType;
|
|
21426
|
+
if (fieldMapping.taskTarget.kind === "override") {
|
|
21427
|
+
task.estimateFields = [...fieldMapping.taskTarget.fields];
|
|
21428
|
+
}
|
|
21429
|
+
}
|
|
21164
21430
|
}
|
|
21165
21431
|
distributeActiveTaskPercentages(calculatedTasks, {
|
|
21166
21432
|
forceNormalize,
|
|
21167
|
-
enableLogging: true
|
|
21433
|
+
enableLogging: true,
|
|
21434
|
+
mode: estimationConfig?.normalize
|
|
21168
21435
|
});
|
|
21169
21436
|
for (const calculatedTask of calculatedTasks) {
|
|
21170
|
-
calculatedTask.estimation = this.calculateEstimation(
|
|
21171
|
-
logger.debug(`Calculated task: ${calculatedTask.title} = ${calculatedTask.estimation}
|
|
21437
|
+
calculatedTask.estimation = resolution.kind === "resolved" ? this.calculateEstimation(resolution.total, calculatedTask, estimationConfig) : calculatedTask.estimationFixed;
|
|
21438
|
+
logger.debug(`Calculated task: ${calculatedTask.title} = ${calculatedTask.estimation ?? "unestimated"} (${calculatedTask.estimationPercent}%)`);
|
|
21172
21439
|
}
|
|
21173
21440
|
logger.info(`EstimationCalculator: Calculated ${calculatedTasks.length} tasks, skipped ${skippedTasks.length} tasks for story ${story.id}`);
|
|
21174
|
-
return {
|
|
21441
|
+
return {
|
|
21442
|
+
calculatedTasks,
|
|
21443
|
+
skippedTasks,
|
|
21444
|
+
...resolution.kind === "blank" ? { unresolvedEstimate: { action: "blank", reason: resolution.reason } } : {},
|
|
21445
|
+
...resolution.kind === "resolved" && resolution.fallbackReason ? { defaultEstimateReason: resolution.fallbackReason } : {}
|
|
21446
|
+
};
|
|
21175
21447
|
}
|
|
21176
|
-
resolveEffectivePercent(task, story) {
|
|
21448
|
+
resolveEffectivePercent(task, story, fieldMapping) {
|
|
21177
21449
|
if (task.estimationPercentCondition?.length) {
|
|
21178
21450
|
for (const rule of task.estimationPercentCondition) {
|
|
21179
|
-
if (this.conditionEvaluator.evaluateCondition(rule.condition, story)) {
|
|
21451
|
+
if (this.conditionEvaluator.evaluateCondition(rule.condition, story, fieldMapping?.storySource)) {
|
|
21180
21452
|
logger.debug(`Task "${task.title}": conditional percent → ${rule.percent}%`);
|
|
21181
21453
|
return rule.percent;
|
|
21182
21454
|
}
|
|
@@ -21193,7 +21465,6 @@ class EstimationCalculator {
|
|
|
21193
21465
|
assignTo: this.resolveAssignment(templateTask.assignTo, story, connectUserEmail),
|
|
21194
21466
|
priority: templateTask.priority,
|
|
21195
21467
|
activity: templateTask.activity,
|
|
21196
|
-
completedWork: 0,
|
|
21197
21468
|
iteration: story.iteration,
|
|
21198
21469
|
areaPath: story.areaPath,
|
|
21199
21470
|
customFields: this.interpolateCustomFields(templateTask.customFields, story),
|
|
@@ -21202,6 +21473,10 @@ class EstimationCalculator {
|
|
|
21202
21473
|
estimationFixed: templateTask.estimationFixed
|
|
21203
21474
|
};
|
|
21204
21475
|
}
|
|
21476
|
+
resolveParentEstimate(story, config2, fieldMapping) {
|
|
21477
|
+
const raw = readStoryEstimate(story, fieldMapping?.storySource ?? { kind: "default", fields: [] });
|
|
21478
|
+
return resolveStoryEstimate(raw, config2);
|
|
21479
|
+
}
|
|
21205
21480
|
calculateEstimation(parentEstimation, task, config2) {
|
|
21206
21481
|
if (task.estimationFixed !== undefined) {
|
|
21207
21482
|
return task.estimationFixed;
|
|
@@ -21209,7 +21484,7 @@ class EstimationCalculator {
|
|
|
21209
21484
|
if (task.estimationPercent !== undefined) {
|
|
21210
21485
|
const rawEstimation = parentEstimation * task.estimationPercent / 100;
|
|
21211
21486
|
const rounded = this.roundEstimation(rawEstimation, config2?.rounding || "none");
|
|
21212
|
-
const minimum = config2?.minimumTaskPoints
|
|
21487
|
+
const minimum = config2?.minimumTaskEstimate ?? config2?.minimumTaskPoints ?? 0;
|
|
21213
21488
|
return Math.max(rounded, minimum);
|
|
21214
21489
|
}
|
|
21215
21490
|
if (task.estimationFormula) {
|
|
@@ -21252,8 +21527,9 @@ class EstimationCalculator {
|
|
|
21252
21527
|
calculateTotalEstimation(tasks) {
|
|
21253
21528
|
return tasks.reduce((sum, task) => sum + (task.estimation || 0), 0);
|
|
21254
21529
|
}
|
|
21255
|
-
getEstimationSummary(story, tasks) {
|
|
21256
|
-
const
|
|
21530
|
+
getEstimationSummary(story, tasks, config2, fieldMapping) {
|
|
21531
|
+
const resolution = this.resolveParentEstimate(story, config2, fieldMapping);
|
|
21532
|
+
const storyEstimation = resolution.kind === "resolved" ? resolution.total : 0;
|
|
21257
21533
|
const totalTaskEstimation = this.calculateTotalEstimation(tasks);
|
|
21258
21534
|
const difference = storyEstimation - totalTaskEstimation;
|
|
21259
21535
|
const percentageUsed = storyEstimation > 0 ? totalTaskEstimation / storyEstimation * 100 : 0;
|
|
@@ -21264,13 +21540,16 @@ class EstimationCalculator {
|
|
|
21264
21540
|
percentageUsed
|
|
21265
21541
|
};
|
|
21266
21542
|
}
|
|
21267
|
-
validateEstimation(story, tasks) {
|
|
21543
|
+
validateEstimation(story, tasks, config2, fieldMapping) {
|
|
21268
21544
|
const warnings = [];
|
|
21269
|
-
|
|
21545
|
+
if (this.resolveParentEstimate(story, config2, fieldMapping).kind !== "resolved") {
|
|
21546
|
+
return { valid: true, warnings };
|
|
21547
|
+
}
|
|
21548
|
+
const summary = this.getEstimationSummary(story, tasks, config2, fieldMapping);
|
|
21270
21549
|
if (Math.abs(summary.difference) > 0.5) {
|
|
21271
21550
|
warnings.push(`Total task estimation (${summary.totalTaskEstimation}) differs from story estimation (${summary.storyEstimation}) by ${summary.difference}`);
|
|
21272
21551
|
}
|
|
21273
|
-
const zeroEstimations = tasks.filter((t2) =>
|
|
21552
|
+
const zeroEstimations = tasks.filter((t2) => t2.estimation === 0);
|
|
21274
21553
|
if (zeroEstimations.length > 0) {
|
|
21275
21554
|
warnings.push(`${zeroEstimations.length} task(s) have zero estimation: ${zeroEstimations.map((t2) => t2.title).join(", ")}`);
|
|
21276
21555
|
}
|
|
@@ -21285,6 +21564,7 @@ var init_estimation_calculator = __esm(() => {
|
|
|
21285
21564
|
init_logger();
|
|
21286
21565
|
init_errors();
|
|
21287
21566
|
init_condition_evaluator();
|
|
21567
|
+
init_estimation_conversion();
|
|
21288
21568
|
init_estimation_distribution();
|
|
21289
21569
|
});
|
|
21290
21570
|
|
|
@@ -21361,27 +21641,54 @@ var init_task_materializer = __esm(() => {
|
|
|
21361
21641
|
class StoryProcessor {
|
|
21362
21642
|
estimationCalculator;
|
|
21363
21643
|
taskMaterializer;
|
|
21644
|
+
estimationDefaults;
|
|
21364
21645
|
constructor(platform, estimationCalculator = new EstimationCalculator, taskMaterializer = new TaskMaterializer(platform)) {
|
|
21365
21646
|
this.estimationCalculator = estimationCalculator;
|
|
21366
21647
|
this.taskMaterializer = taskMaterializer;
|
|
21648
|
+
this.estimationDefaults = platform.getEstimationDefaults?.();
|
|
21367
21649
|
}
|
|
21368
21650
|
async process(story, orderedTasks, template, connectUserEmail, options, warnings) {
|
|
21369
21651
|
logger.info(`Processing: ${story.id} - ${story.title}`);
|
|
21370
|
-
const
|
|
21652
|
+
const fieldMapping = this.estimationDefaults ? resolveEstimationFieldMapping(this.estimationDefaults, {
|
|
21653
|
+
source: template.estimation?.source,
|
|
21654
|
+
taskType: template.taskType,
|
|
21655
|
+
targetFields: template.estimation?.targetFields
|
|
21656
|
+
}) : undefined;
|
|
21657
|
+
const { calculatedTasks, skippedTasks, unresolvedEstimate, defaultEstimateReason } = this.estimationCalculator.calculateTasksWithSkipped(story, connectUserEmail, orderedTasks, template.estimation, options.forceNormalize, fieldMapping);
|
|
21371
21658
|
logger.info(`Generated ${calculatedTasks.length} tasks, skipped ${skippedTasks.length} conditional tasks`);
|
|
21659
|
+
if (unresolvedEstimate?.action === "skip") {
|
|
21660
|
+
const skipReason = `${unresolvedEstimate.reason}; Story skipped (ifParentHasNoEstimation: skip)`;
|
|
21661
|
+
warnings.push(`${story.id}: ${skipReason}`);
|
|
21662
|
+
return {
|
|
21663
|
+
story,
|
|
21664
|
+
tasksCalculated: [],
|
|
21665
|
+
tasksCreated: [],
|
|
21666
|
+
tasksSkipped: [],
|
|
21667
|
+
success: true,
|
|
21668
|
+
...fieldMapping?.unitLabel ? { estimateUnit: fieldMapping.unitLabel } : {},
|
|
21669
|
+
...withStoryEstimate(readStoryEstimate(story, fieldMapping?.storySource ?? { kind: "default", fields: [] })),
|
|
21670
|
+
skipReason
|
|
21671
|
+
};
|
|
21672
|
+
}
|
|
21673
|
+
if (unresolvedEstimate?.action === "blank") {
|
|
21674
|
+
warnings.push(`${story.id}: ${unresolvedEstimate.reason}; Task Estimates left blank`);
|
|
21675
|
+
}
|
|
21676
|
+
if (defaultEstimateReason) {
|
|
21677
|
+
warnings.push(`${story.id}: ${defaultEstimateReason}; used defaultParentEstimation`);
|
|
21678
|
+
}
|
|
21372
21679
|
if (skippedTasks.length > 0) {
|
|
21373
21680
|
skippedTasks.forEach((skipped) => {
|
|
21374
21681
|
logger.debug(` Skipped: "${skipped.templateTask.title}" - ${skipped.reason}`);
|
|
21375
21682
|
});
|
|
21376
21683
|
}
|
|
21377
|
-
const validation = this.estimationCalculator.validateEstimation(story, calculatedTasks);
|
|
21684
|
+
const validation = this.estimationCalculator.validateEstimation(story, calculatedTasks, template.estimation, fieldMapping);
|
|
21378
21685
|
if (!validation.valid) {
|
|
21379
21686
|
validation.warnings.forEach((warning) => {
|
|
21380
21687
|
logger.warn(`${warning}`);
|
|
21381
21688
|
warnings.push(`${story.id}: ${warning}`);
|
|
21382
21689
|
});
|
|
21383
21690
|
}
|
|
21384
|
-
const estimationSummary = this.estimationCalculator.getEstimationSummary(story, calculatedTasks);
|
|
21691
|
+
const estimationSummary = this.estimationCalculator.getEstimationSummary(story, calculatedTasks, template.estimation, fieldMapping);
|
|
21385
21692
|
logger.debug("Estimation summary:", estimationSummary);
|
|
21386
21693
|
let tasksCreated = [];
|
|
21387
21694
|
if (options.dryRun) {
|
|
@@ -21397,10 +21704,15 @@ class StoryProcessor {
|
|
|
21397
21704
|
tasksCreated,
|
|
21398
21705
|
tasksSkipped: skippedTasks,
|
|
21399
21706
|
success: true,
|
|
21707
|
+
...fieldMapping?.unitLabel ? { estimateUnit: fieldMapping.unitLabel } : {},
|
|
21708
|
+
...withStoryEstimate(readStoryEstimate(story, fieldMapping?.storySource ?? { kind: "default", fields: [] })),
|
|
21400
21709
|
estimationSummary
|
|
21401
21710
|
};
|
|
21402
21711
|
}
|
|
21403
21712
|
}
|
|
21713
|
+
function withStoryEstimate(estimate) {
|
|
21714
|
+
return estimate === undefined ? {} : { storyEstimate: estimate };
|
|
21715
|
+
}
|
|
21404
21716
|
var init_story_processor = __esm(() => {
|
|
21405
21717
|
init_logger();
|
|
21406
21718
|
init_estimation_calculator();
|
|
@@ -79871,29 +80183,33 @@ class FieldSchemaService {
|
|
|
79871
80183
|
const allowedValues = witField.allowedValues && witField.allowedValues.length > 0 ? witField.allowedValues : undefined;
|
|
79872
80184
|
const isPicklist = !!(allowedValues ?? meta3?.isPicklist);
|
|
79873
80185
|
const isIdentity = meta3?.isIdentity === true || fieldType === import_WorkItemTrackingInterfaces.FieldType.Identity;
|
|
80186
|
+
const type = isIdentity ? "identity" : this.mapFieldType(fieldType);
|
|
79874
80187
|
return {
|
|
79875
80188
|
referenceName,
|
|
79876
80189
|
name: witField.name ?? meta3?.name ?? referenceName,
|
|
79877
|
-
type
|
|
80190
|
+
type,
|
|
79878
80191
|
isCustom: referenceName.startsWith("Custom."),
|
|
79879
80192
|
isReadOnly: meta3?.readOnly ?? false,
|
|
79880
80193
|
isMultiline: !isPicklist && (fieldType === import_WorkItemTrackingInterfaces.FieldType.Html || fieldType === import_WorkItemTrackingInterfaces.FieldType.PlainText),
|
|
79881
80194
|
isPicklist,
|
|
79882
|
-
allowedValues
|
|
80195
|
+
allowedValues,
|
|
80196
|
+
allowsCustomValues: isPicklist ? meta3?.isPicklistSuggested === true : type === "string"
|
|
79883
80197
|
};
|
|
79884
80198
|
}
|
|
79885
80199
|
mapWorkItemField(f) {
|
|
79886
80200
|
const referenceName = f.referenceName ?? "";
|
|
79887
80201
|
const isIdentity = f.isIdentity === true || f.type === import_WorkItemTrackingInterfaces.FieldType.Identity;
|
|
80202
|
+
const type = isIdentity ? "identity" : this.mapFieldType(f.type);
|
|
79888
80203
|
return {
|
|
79889
80204
|
referenceName,
|
|
79890
80205
|
name: f.name ?? referenceName,
|
|
79891
|
-
type
|
|
80206
|
+
type,
|
|
79892
80207
|
isCustom: referenceName.startsWith("Custom."),
|
|
79893
80208
|
isReadOnly: f.readOnly ?? false,
|
|
79894
80209
|
isMultiline: !f.isPicklist && (f.type === import_WorkItemTrackingInterfaces.FieldType.Html || f.type === import_WorkItemTrackingInterfaces.FieldType.PlainText),
|
|
79895
80210
|
isPicklist: f.isPicklist ?? false,
|
|
79896
|
-
allowedValues: undefined
|
|
80211
|
+
allowedValues: undefined,
|
|
80212
|
+
allowsCustomValues: f.isPicklist ? f.isPicklistSuggested === true : type === "string"
|
|
79897
80213
|
};
|
|
79898
80214
|
}
|
|
79899
80215
|
mapFieldType(fieldType) {
|
|
@@ -79920,6 +80236,33 @@ var init_azure_devops_field_schema_service = __esm(() => {
|
|
|
79920
80236
|
import_WorkItemTrackingInterfaces = __toESM(require_WorkItemTrackingInterfaces(), 1);
|
|
79921
80237
|
});
|
|
79922
80238
|
|
|
80239
|
+
// ../atomize-core/src/platforms/adapters/azure-devops/estimation-defaults.ts
|
|
80240
|
+
var AZURE_DEVOPS_ESTIMATION_DEFAULTS;
|
|
80241
|
+
var init_estimation_defaults = __esm(() => {
|
|
80242
|
+
AZURE_DEVOPS_ESTIMATION_DEFAULTS = {
|
|
80243
|
+
storyEstimateFields: [
|
|
80244
|
+
"Microsoft.VSTS.Scheduling.StoryPoints",
|
|
80245
|
+
"Microsoft.VSTS.Scheduling.Effort",
|
|
80246
|
+
"Microsoft.VSTS.Scheduling.Size",
|
|
80247
|
+
"Microsoft.VSTS.Scheduling.OriginalEstimate"
|
|
80248
|
+
],
|
|
80249
|
+
taskWorkItemType: "Task",
|
|
80250
|
+
taskEstimateFields: [
|
|
80251
|
+
"Microsoft.VSTS.Scheduling.RemainingWork",
|
|
80252
|
+
"Microsoft.VSTS.Scheduling.OriginalEstimate"
|
|
80253
|
+
],
|
|
80254
|
+
unitLabel: "hours",
|
|
80255
|
+
nonEstimateFields: [
|
|
80256
|
+
"Microsoft.VSTS.Common.*",
|
|
80257
|
+
"Microsoft.VSTS.Build.*",
|
|
80258
|
+
"Microsoft.VSTS.CMMI.*",
|
|
80259
|
+
"Microsoft.VSTS.CodeReview.*",
|
|
80260
|
+
"Microsoft.VSTS.Feedback.*",
|
|
80261
|
+
"Microsoft.VSTS.TCM.*"
|
|
80262
|
+
]
|
|
80263
|
+
};
|
|
80264
|
+
});
|
|
80265
|
+
|
|
79923
80266
|
// ../atomize-core/src/platforms/adapters/azure-devops/task-patch-builder.ts
|
|
79924
80267
|
function resolveDateMacro(value) {
|
|
79925
80268
|
if (typeof value !== "string")
|
|
@@ -79951,7 +80294,7 @@ function resolveDateMacro(value) {
|
|
|
79951
80294
|
base.setDate(base.getDate() + offsetDays);
|
|
79952
80295
|
return base.toISOString().slice(0, 10);
|
|
79953
80296
|
}
|
|
79954
|
-
function buildCreateTaskPatch(organizationUrl, parentId, task) {
|
|
80297
|
+
function buildCreateTaskPatch(organizationUrl, parentId, task, estimateWrite) {
|
|
79955
80298
|
return [
|
|
79956
80299
|
{
|
|
79957
80300
|
op: "add",
|
|
@@ -79965,25 +80308,12 @@ function buildCreateTaskPatch(organizationUrl, parentId, task) {
|
|
|
79965
80308
|
value: task.description
|
|
79966
80309
|
}
|
|
79967
80310
|
] : [],
|
|
79968
|
-
...task.estimation !== undefined ?
|
|
79969
|
-
|
|
79970
|
-
|
|
79971
|
-
|
|
79972
|
-
|
|
79973
|
-
|
|
79974
|
-
{
|
|
79975
|
-
op: "add",
|
|
79976
|
-
path: "/fields/Microsoft.VSTS.Scheduling.OriginalEstimate",
|
|
79977
|
-
value: task.estimation
|
|
79978
|
-
}
|
|
79979
|
-
] : [],
|
|
79980
|
-
...task.completedWork !== undefined ? [
|
|
79981
|
-
{
|
|
79982
|
-
op: "add",
|
|
79983
|
-
path: "/fields/Microsoft.VSTS.Scheduling.CompletedWork",
|
|
79984
|
-
value: task.completedWork
|
|
79985
|
-
}
|
|
79986
|
-
] : [],
|
|
80311
|
+
...task.estimation !== undefined ? estimateWrite.fields.map((field) => ({
|
|
80312
|
+
op: "add",
|
|
80313
|
+
path: `/fields/${field}`,
|
|
80314
|
+
value: task.estimation
|
|
80315
|
+
})) : [],
|
|
80316
|
+
...estimateWrite.initializeCompletedWork ? [{ op: "add", path: "/fields/Microsoft.VSTS.Scheduling.CompletedWork", value: 0 }] : [],
|
|
79987
80317
|
...task.iteration ? [
|
|
79988
80318
|
{
|
|
79989
80319
|
op: "add",
|
|
@@ -80065,6 +80395,14 @@ var init_task_patch_builder = __esm(() => {
|
|
|
80065
80395
|
});
|
|
80066
80396
|
|
|
80067
80397
|
// ../atomize-core/src/platforms/adapters/azure-devops/work-item-mapper.ts
|
|
80398
|
+
function firstEstimate(fields) {
|
|
80399
|
+
for (const field of AZURE_DEVOPS_ESTIMATION_DEFAULTS.storyEstimateFields) {
|
|
80400
|
+
const value = fields[field];
|
|
80401
|
+
if ((typeof value === "number" || typeof value === "string") && value)
|
|
80402
|
+
return value;
|
|
80403
|
+
}
|
|
80404
|
+
return;
|
|
80405
|
+
}
|
|
80068
80406
|
function convertWorkItem(azureItem) {
|
|
80069
80407
|
const fields = azureItem.fields || {};
|
|
80070
80408
|
const predecessorIds = [];
|
|
@@ -80091,7 +80429,7 @@ function convertWorkItem(azureItem) {
|
|
|
80091
80429
|
type: fields["System.WorkItemType"],
|
|
80092
80430
|
state: fields["System.State"] || "",
|
|
80093
80431
|
assignedTo: fields["System.AssignedTo"]?.uniqueName || fields["System.AssignedTo"]?.displayName,
|
|
80094
|
-
estimation: fields
|
|
80432
|
+
estimation: firstEstimate(fields),
|
|
80095
80433
|
tags: fields["System.Tags"] ? fields["System.Tags"].split("; ") : [],
|
|
80096
80434
|
description: fields["System.Description"],
|
|
80097
80435
|
areaPath: fields["System.AreaPath"],
|
|
@@ -80115,10 +80453,8 @@ function hasChildRelations(workItem) {
|
|
|
80115
80453
|
}
|
|
80116
80454
|
return workItem.relations.some((rel) => rel.rel === "System.LinkTypes.Hierarchy-Forward");
|
|
80117
80455
|
}
|
|
80118
|
-
|
|
80119
|
-
|
|
80120
|
-
var init_schema2 = __esm(() => {
|
|
80121
|
-
init_src();
|
|
80456
|
+
var init_work_item_mapper = __esm(() => {
|
|
80457
|
+
init_estimation_defaults();
|
|
80122
80458
|
});
|
|
80123
80459
|
|
|
80124
80460
|
// ../atomize-core/src/platforms/adapters/azure-devops/work-item-query.ts
|
|
@@ -80383,6 +80719,7 @@ var import_WorkItemTrackingInterfaces2;
|
|
|
80383
80719
|
var init_work_item_retrieval = __esm(() => {
|
|
80384
80720
|
init_logger();
|
|
80385
80721
|
init_errors();
|
|
80722
|
+
init_work_item_mapper();
|
|
80386
80723
|
init_work_item_query();
|
|
80387
80724
|
import_WorkItemTrackingInterfaces2 = __toESM(require_WorkItemTrackingInterfaces(), 1);
|
|
80388
80725
|
});
|
|
@@ -80490,11 +80827,13 @@ class AzureDevOpsAdapter {
|
|
|
80490
80827
|
if (Number.isNaN(numericParentId)) {
|
|
80491
80828
|
throw new Error(`Invalid parent ID: ${parentId}`);
|
|
80492
80829
|
}
|
|
80493
|
-
const
|
|
80830
|
+
const defaults = this.getEstimationDefaults();
|
|
80831
|
+
const estimateWrite = task.estimateFields?.length ? { fields: task.estimateFields } : { fields: defaults.taskEstimateFields, initializeCompletedWork: true };
|
|
80832
|
+
const patchDocument = buildCreateTaskPatch(this.config.organizationUrl, numericParentId, task, estimateWrite);
|
|
80494
80833
|
if (!this.witApi) {
|
|
80495
80834
|
throw new UnknownError("Work Item Tracking API not initialized");
|
|
80496
80835
|
}
|
|
80497
|
-
const createdItem = await this.witApi.createWorkItem(undefined, patchDocument, this.config.project,
|
|
80836
|
+
const createdItem = await this.witApi.createWorkItem(undefined, patchDocument, this.config.project, task.workItemType ?? defaults.taskWorkItemType);
|
|
80498
80837
|
logger.info(`AzureDevOps: Created task ${createdItem.id}: ${task.title}`);
|
|
80499
80838
|
return convertWorkItem(createdItem);
|
|
80500
80839
|
} catch (error62) {
|
|
@@ -80531,6 +80870,9 @@ class AzureDevOpsAdapter {
|
|
|
80531
80870
|
logger.info(`AzureDevOps: Created ${createdTasks.length} of ${tasks.length} tasks`);
|
|
80532
80871
|
return createdTasks;
|
|
80533
80872
|
}
|
|
80873
|
+
getEstimationDefaults() {
|
|
80874
|
+
return AZURE_DEVOPS_ESTIMATION_DEFAULTS;
|
|
80875
|
+
}
|
|
80534
80876
|
getPlatformMetadata() {
|
|
80535
80877
|
return {
|
|
80536
80878
|
name: "Azure DevOps",
|
|
@@ -80743,7 +81085,9 @@ var init_azure_devops_adapter = __esm(() => {
|
|
|
80743
81085
|
init_logger();
|
|
80744
81086
|
init_errors();
|
|
80745
81087
|
init_azure_devops_field_schema_service();
|
|
81088
|
+
init_estimation_defaults();
|
|
80746
81089
|
init_task_patch_builder();
|
|
81090
|
+
init_work_item_mapper();
|
|
80747
81091
|
init_work_item_query();
|
|
80748
81092
|
init_work_item_retrieval();
|
|
80749
81093
|
azdev = __toESM(require_WebApi(), 1);
|
|
@@ -80995,7 +81339,7 @@ class MockPlatformAdapter {
|
|
|
80995
81339
|
const createdTask = {
|
|
80996
81340
|
id: `TASK-${this.taskIdCounter++}`,
|
|
80997
81341
|
title: task.title,
|
|
80998
|
-
type:
|
|
81342
|
+
type: task.workItemType ?? MOCK_ESTIMATION_DEFAULTS.taskWorkItemType,
|
|
80999
81343
|
state: "New",
|
|
81000
81344
|
assignedTo: task.assignTo,
|
|
81001
81345
|
estimation: task.estimation,
|
|
@@ -81063,6 +81407,9 @@ class MockPlatformAdapter {
|
|
|
81063
81407
|
logger.info(`MockPlatform: Deleted work item ${id}`);
|
|
81064
81408
|
return true;
|
|
81065
81409
|
}
|
|
81410
|
+
getEstimationDefaults() {
|
|
81411
|
+
return MOCK_ESTIMATION_DEFAULTS;
|
|
81412
|
+
}
|
|
81066
81413
|
getPlatformMetadata() {
|
|
81067
81414
|
return {
|
|
81068
81415
|
name: "Mock Platform",
|
|
@@ -81109,9 +81456,16 @@ class MockPlatformAdapter {
|
|
|
81109
81456
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81110
81457
|
}
|
|
81111
81458
|
}
|
|
81459
|
+
var MOCK_ESTIMATION_DEFAULTS;
|
|
81112
81460
|
var init_mock_adapter = __esm(() => {
|
|
81113
81461
|
init_logger();
|
|
81114
81462
|
init_mock_data();
|
|
81463
|
+
MOCK_ESTIMATION_DEFAULTS = {
|
|
81464
|
+
storyEstimateFields: ["estimation"],
|
|
81465
|
+
taskWorkItemType: "Task",
|
|
81466
|
+
taskEstimateFields: ["estimation"],
|
|
81467
|
+
unitLabel: "hours"
|
|
81468
|
+
};
|
|
81115
81469
|
});
|
|
81116
81470
|
|
|
81117
81471
|
// ../atomize-core/src/platforms/platform-factory.ts
|
|
@@ -88764,6 +89118,7 @@ function mergeTemplates(base, child) {
|
|
|
88764
89118
|
lastModified: child.lastModified ?? base.lastModified,
|
|
88765
89119
|
filter: child.filter ?? base.filter,
|
|
88766
89120
|
tasks: mergeTasks(base.tasks, child.tasks ?? []),
|
|
89121
|
+
taskType: child.taskType ?? base.taskType,
|
|
88767
89122
|
estimation: child.estimation ?? base.estimation,
|
|
88768
89123
|
validation: child.validation ?? base.validation,
|
|
88769
89124
|
metadata: child.metadata ?? base.metadata
|
|
@@ -89195,12 +89550,13 @@ class TemplateValidator {
|
|
|
89195
89550
|
collectWarnings(template) {
|
|
89196
89551
|
const warnings = [];
|
|
89197
89552
|
const v2 = template.validation;
|
|
89198
|
-
const total = template.tasks.reduce((sum, t2) => sum + (t2.estimationPercent ?? 0), 0);
|
|
89553
|
+
const total = template.tasks.reduce((sum, t2) => sum + (t2.estimationPercent ?? 0) * copyCount(t2), 0);
|
|
89199
89554
|
const hasStrictRule = v2?.totalEstimationMustBe !== undefined || v2?.totalEstimationRange !== undefined;
|
|
89200
|
-
|
|
89555
|
+
const normalize = template.estimation?.normalize ?? "auto";
|
|
89556
|
+
if (!hasStrictRule && total > 100 && normalize !== "always") {
|
|
89201
89557
|
warnings.push({
|
|
89202
89558
|
path: "tasks",
|
|
89203
|
-
message: `Total estimation is ${total}% (exceeds 100%). This is valid when tasks span multiple roles. You will be prompted to normalise at generate time.`,
|
|
89559
|
+
message: normalize === "never" ? `Total estimation is ${total}% (exceeds 100%). With normalize: never, tasks are generated with these percentages as written.` : `Total estimation is ${total}% (exceeds 100%). This is valid when tasks span multiple roles. You will be prompted to normalise at generate time.`,
|
|
89204
89560
|
nonBlocking: true
|
|
89205
89561
|
});
|
|
89206
89562
|
}
|
|
@@ -89215,8 +89571,31 @@ class TemplateValidator {
|
|
|
89215
89571
|
this.validateTaskConditions(template, warnings);
|
|
89216
89572
|
this.validateTaskDependencies(template, warnings);
|
|
89217
89573
|
this.validateSavedQueryConflict(template, warnings);
|
|
89574
|
+
this.validateDeprecatedMinimumTaskPoints(template, warnings);
|
|
89218
89575
|
return warnings;
|
|
89219
89576
|
}
|
|
89577
|
+
validateDeprecatedMinimumTaskPoints(template, warnings) {
|
|
89578
|
+
const estimation = template.estimation;
|
|
89579
|
+
if (estimation?.minimumTaskPoints === undefined)
|
|
89580
|
+
return;
|
|
89581
|
+
if (estimation.minimumTaskEstimate === undefined) {
|
|
89582
|
+
warnings.push({
|
|
89583
|
+
path: "estimation.minimumTaskPoints",
|
|
89584
|
+
message: "minimumTaskPoints is deprecated; the minimum applies in the Task's unit, which may not be points.",
|
|
89585
|
+
suggestion: `Rename it to minimumTaskEstimate: ${estimation.minimumTaskPoints}.`,
|
|
89586
|
+
code: FixableWarningCode.DEPRECATED_MINIMUM_TASK_POINTS,
|
|
89587
|
+
nonBlocking: true
|
|
89588
|
+
});
|
|
89589
|
+
return;
|
|
89590
|
+
}
|
|
89591
|
+
warnings.push({
|
|
89592
|
+
path: "estimation.minimumTaskPoints",
|
|
89593
|
+
message: `Both minimumTaskEstimate and the deprecated minimumTaskPoints are set; minimumTaskEstimate (${estimation.minimumTaskEstimate}) is used and minimumTaskPoints (${estimation.minimumTaskPoints}) is ignored.`,
|
|
89594
|
+
suggestion: "Remove minimumTaskPoints.",
|
|
89595
|
+
code: FixableWarningCode.DUPLICATE_MINIMUM_TASK_ESTIMATE,
|
|
89596
|
+
nonBlocking: true
|
|
89597
|
+
});
|
|
89598
|
+
}
|
|
89220
89599
|
collectMixinWarnings(mixin) {
|
|
89221
89600
|
const warnings = [];
|
|
89222
89601
|
this.validateTaskDependencies(mixin, warnings);
|
|
@@ -89266,13 +89645,13 @@ class TemplateValidator {
|
|
|
89266
89645
|
const f = template.filter;
|
|
89267
89646
|
if (!f.savedQuery)
|
|
89268
89647
|
return;
|
|
89269
|
-
const hasStructuredFields = f.
|
|
89648
|
+
const hasStructuredFields = f.states || f.statesExclude || f.statesWereEver || f.tags?.include || f.tags?.exclude || f.areaPaths || f.areaPathsUnder || f.iterations || f.iterationsUnder || f.assignedTo || f.changedAfter || f.createdAfter || f.priority;
|
|
89270
89649
|
if (hasStructuredFields) {
|
|
89271
89650
|
warnings.push({
|
|
89272
89651
|
path: "filter.savedQuery",
|
|
89273
89652
|
message: "savedQuery and structured filter fields are both set. Structured filter fields will be ignored — the saved query controls which items are returned.",
|
|
89274
89653
|
code: FixableWarningCode.SAVED_QUERY_WITH_STRUCTURED_FILTER,
|
|
89275
|
-
suggestion: "Remove
|
|
89654
|
+
suggestion: "Remove states, tags, area paths, etc. from the filter when using savedQuery. Keep workItemTypes: it declares which types the query returns."
|
|
89276
89655
|
});
|
|
89277
89656
|
}
|
|
89278
89657
|
}
|
|
@@ -89297,22 +89676,32 @@ class TemplateValidator {
|
|
|
89297
89676
|
return this.handleZodError(err);
|
|
89298
89677
|
}
|
|
89299
89678
|
handleDomainError(err, code) {
|
|
89679
|
+
const params = err.code === "custom" ? err.params : undefined;
|
|
89680
|
+
const repeatHint = params?.hasRepeats ? " Each copy of a repeated task counts toward this total." : "";
|
|
89300
89681
|
switch (code) {
|
|
89301
89682
|
case "INVALID_TOTAL_ESTIMATION":
|
|
89302
|
-
return this.handleNumericError(err.message, /is (\d+)%, but must be (\d+)%/, (current, required2) => {
|
|
89683
|
+
return this.withHint(repeatHint, this.handleNumericError(err.message, /is (\d+)%, but must be (\d+)%/, (current, required2) => {
|
|
89303
89684
|
const diff = required2 - current;
|
|
89304
89685
|
return diff > 0 ? `Add ${diff}% to existing tasks or create a new task with ${diff}% estimation.` : `Reduce task estimations by ${Math.abs(diff)}% to reach ${required2}%.`;
|
|
89305
|
-
});
|
|
89686
|
+
}));
|
|
89306
89687
|
case "INVALID_ESTIMATION_RANGE":
|
|
89307
|
-
return this.handleNumericError(err.message, /is (\d+)%, but must be between (\d+)% and (\d+)%/, (current, min, max) => {
|
|
89688
|
+
return this.withHint(repeatHint, this.handleNumericError(err.message, /is (\d+)%, but must be between (\d+)% and (\d+)%/, (current, min, max) => {
|
|
89308
89689
|
if (current < min)
|
|
89309
89690
|
return `Increase task estimations by ${min - current}% to meet the minimum of ${min}%.`;
|
|
89310
89691
|
return `Reduce task estimations by ${current - max}% to stay within the maximum of ${max}%.`;
|
|
89311
|
-
});
|
|
89312
|
-
case "TOO_FEW_TASKS":
|
|
89313
|
-
|
|
89314
|
-
|
|
89315
|
-
|
|
89692
|
+
}));
|
|
89693
|
+
case "TOO_FEW_TASKS": {
|
|
89694
|
+
if (typeof params?.count !== "number" || typeof params.limit !== "number")
|
|
89695
|
+
return;
|
|
89696
|
+
const { count, limit } = params;
|
|
89697
|
+
return params.hasRepeats ? `Add ${limit - count} more task(s) or raise a repeat count to meet the minimum requirement of ${limit} tasks.` : `Add ${limit - count} more task(s) to meet the minimum requirement of ${limit} tasks.`;
|
|
89698
|
+
}
|
|
89699
|
+
case "TOO_MANY_TASKS": {
|
|
89700
|
+
if (typeof params?.count !== "number" || typeof params.limit !== "number")
|
|
89701
|
+
return;
|
|
89702
|
+
const { count, limit } = params;
|
|
89703
|
+
return params.hasRepeats ? `Remove ${count - limit} task(s), lower a repeat count, or increase the maxTasks limit to ${count}.` : `Remove ${count - limit} task(s) or increase the maxTasks limit to ${count}.`;
|
|
89704
|
+
}
|
|
89316
89705
|
case "INVALID_DEPENDENCY": {
|
|
89317
89706
|
const match = err.message.match(/non-existent task ID: "([^"]+)"/);
|
|
89318
89707
|
return match ? `Either add a task with id: "${match[1]}" or update the dependsOn field to reference an existing task ID.` : undefined;
|
|
@@ -89325,6 +89714,14 @@ class TemplateValidator {
|
|
|
89325
89714
|
}
|
|
89326
89715
|
return "Remove one of the dependencies in the cycle to break the circular reference.";
|
|
89327
89716
|
}
|
|
89717
|
+
case "REPEAT_EXCEEDS_MAX":
|
|
89718
|
+
return "Lower the repeat count, or raise validation.maxRepeat if the template genuinely needs more copies.";
|
|
89719
|
+
case "REPEAT_WITH_DEPENDENCY":
|
|
89720
|
+
return "Remove dependsOn from the repeated task, or remove repeat and define each copy as its own task.";
|
|
89721
|
+
case "DEPENDS_ON_REPEATED_TASK": {
|
|
89722
|
+
const match = err.message.match(/depends on "([^"]+)"/);
|
|
89723
|
+
return match ? `Remove "${match[1]}" from dependsOn, or remove repeat from task "${match[1]}".` : "Remove the dependency on the repeated task, or remove its repeat.";
|
|
89724
|
+
}
|
|
89328
89725
|
case "MISSING_REQUIRED_TASK": {
|
|
89329
89726
|
const match = err.message.match(/title "([^"]+)"/);
|
|
89330
89727
|
return match ? `Add a task with title "${match[1]}" to satisfy the required tasks constraint.` : "Add the missing required task to the template.";
|
|
@@ -89333,6 +89730,9 @@ class TemplateValidator {
|
|
|
89333
89730
|
return;
|
|
89334
89731
|
}
|
|
89335
89732
|
}
|
|
89733
|
+
withHint(hint, suggestion) {
|
|
89734
|
+
return suggestion && `${suggestion}${hint}`;
|
|
89735
|
+
}
|
|
89336
89736
|
handleNumericError(message, regex, formatter) {
|
|
89337
89737
|
const values = this.extractValues(message, regex);
|
|
89338
89738
|
if (!values || values.length === 0)
|
|
@@ -89417,15 +89817,18 @@ var init_validator = __esm(() => {
|
|
|
89417
89817
|
FixableWarningCode = {
|
|
89418
89818
|
MISSING_TASK_ID: "MISSING_TASK_ID",
|
|
89419
89819
|
SAVED_QUERY_WITH_STRUCTURED_FILTER: "SAVED_QUERY_WITH_STRUCTURED_FILTER",
|
|
89420
|
-
SINGLE_LINE_FIELD_WITH_NEWLINES: "SINGLE_LINE_FIELD_WITH_NEWLINES"
|
|
89820
|
+
SINGLE_LINE_FIELD_WITH_NEWLINES: "SINGLE_LINE_FIELD_WITH_NEWLINES",
|
|
89821
|
+
DEPRECATED_MINIMUM_TASK_POINTS: "DEPRECATED_MINIMUM_TASK_POINTS",
|
|
89822
|
+
DUPLICATE_MINIMUM_TASK_ESTIMATE: "DUPLICATE_MINIMUM_TASK_ESTIMATE"
|
|
89421
89823
|
};
|
|
89422
89824
|
});
|
|
89423
89825
|
|
|
89424
89826
|
// ../atomize-core/src/templates/custom-field-verifier.ts
|
|
89425
|
-
async function verifyTemplateCustomFields(template, getSchemas) {
|
|
89827
|
+
async function verifyTemplateCustomFields(template, getSchemas, taskWorkItemType) {
|
|
89426
89828
|
const errors4 = [];
|
|
89427
89829
|
const warnings = [];
|
|
89428
|
-
const taskSchemas = await getSchemas(
|
|
89830
|
+
const taskSchemas = await getSchemas(taskWorkItemType);
|
|
89831
|
+
const scope = taskWorkItemType ? `work item type "${taskWorkItemType}"` : "this project";
|
|
89429
89832
|
const schemaByRef = new Map(taskSchemas.map((field) => [field.referenceName, field]));
|
|
89430
89833
|
for (let i2 = 0;i2 < template.tasks.length; i2++) {
|
|
89431
89834
|
const task = template.tasks[i2];
|
|
@@ -89437,7 +89840,7 @@ async function verifyTemplateCustomFields(template, getSchemas) {
|
|
|
89437
89840
|
if (!schema2) {
|
|
89438
89841
|
errors4.push({
|
|
89439
89842
|
path,
|
|
89440
|
-
message: `Field "${refName}" not found for
|
|
89843
|
+
message: `Field "${refName}" not found for ${scope}.`,
|
|
89441
89844
|
code: "CUSTOM_FIELD_NOT_FOUND"
|
|
89442
89845
|
});
|
|
89443
89846
|
continue;
|
|
@@ -89589,16 +89992,166 @@ var init_custom_field_verifier = __esm(() => {
|
|
|
89589
89992
|
init_validator();
|
|
89590
89993
|
});
|
|
89591
89994
|
|
|
89995
|
+
// ../atomize-core/src/templates/estimation-verifier.ts
|
|
89996
|
+
function hasEstimationMappingOverrides(template) {
|
|
89997
|
+
return !!(template.taskType || template.estimation?.source || template.estimation?.targetFields?.length);
|
|
89998
|
+
}
|
|
89999
|
+
async function verifyEstimationMapping(template, platform) {
|
|
90000
|
+
const errors4 = [];
|
|
90001
|
+
const warnings = [];
|
|
90002
|
+
if (!hasEstimationMappingOverrides(template))
|
|
90003
|
+
return { errors: errors4, warnings };
|
|
90004
|
+
const taskType = template.taskType ?? platform.getEstimationDefaults?.().taskWorkItemType;
|
|
90005
|
+
const nonEstimateFields = platform.getEstimationDefaults?.().nonEstimateFields ?? [];
|
|
90006
|
+
const taskTypeExists = await verifyTaskType(template, platform, errors4);
|
|
90007
|
+
const targetFields = template.estimation?.targetFields ?? [];
|
|
90008
|
+
if (targetFields.length > 0 && taskType && taskTypeExists) {
|
|
90009
|
+
const taskFields = byReference(await platform.getFieldSchemas(taskType));
|
|
90010
|
+
targetFields.forEach((ref, i2) => {
|
|
90011
|
+
const path = `estimation.targetFields[${i2}]`;
|
|
90012
|
+
const field = taskFields.get(ref);
|
|
90013
|
+
if (!field) {
|
|
90014
|
+
errors4.push({ path, message: `Field "${ref}" not found for work item type "${taskType}".`, code: "ESTIMATION_TARGET_FIELD_NOT_FOUND" });
|
|
90015
|
+
} else if (!NUMERIC_TYPES.has(field.type)) {
|
|
90016
|
+
errors4.push({
|
|
90017
|
+
path,
|
|
90018
|
+
message: `Field "${ref}" is a ${field.type} field; Task Estimates can only be written to numeric fields.`,
|
|
90019
|
+
code: "ESTIMATION_TARGET_FIELD_NOT_NUMERIC"
|
|
90020
|
+
});
|
|
90021
|
+
} else if (field.isPicklist) {
|
|
90022
|
+
errors4.push({
|
|
90023
|
+
path,
|
|
90024
|
+
message: `Field "${ref}" only accepts a fixed list of values, so calculated Task Estimates can't be written to it.`,
|
|
90025
|
+
code: "ESTIMATION_TARGET_FIELD_PICKLIST"
|
|
90026
|
+
});
|
|
90027
|
+
} else if (matchesFieldPattern(ref, nonEstimateFields)) {
|
|
90028
|
+
warnings.push({
|
|
90029
|
+
path,
|
|
90030
|
+
message: `Field "${ref}" holds a ranking or value, not effort; writing Task Estimates to it will overwrite that.`,
|
|
90031
|
+
suggestion: "Choose an effort field such as the platform's default estimate fields or a custom effort field."
|
|
90032
|
+
});
|
|
90033
|
+
}
|
|
90034
|
+
});
|
|
90035
|
+
}
|
|
90036
|
+
const source = template.estimation?.source;
|
|
90037
|
+
if (source) {
|
|
90038
|
+
const sourceField = await verifySourceField(template, source, platform, errors4);
|
|
90039
|
+
if (sourceField)
|
|
90040
|
+
checkSourceValues(template, sourceField, warnings);
|
|
90041
|
+
}
|
|
90042
|
+
return { errors: errors4, warnings };
|
|
90043
|
+
}
|
|
90044
|
+
async function verifyTaskType(template, platform, errors4) {
|
|
90045
|
+
if (!template.taskType || !platform.getWorkItemTypes)
|
|
90046
|
+
return true;
|
|
90047
|
+
const types = await platform.getWorkItemTypes();
|
|
90048
|
+
if (types.includes(template.taskType))
|
|
90049
|
+
return true;
|
|
90050
|
+
errors4.push({
|
|
90051
|
+
path: "taskType",
|
|
90052
|
+
message: `Work item type "${template.taskType}" does not exist in this project. Available types: ${types.join(", ")}.`,
|
|
90053
|
+
code: "ESTIMATION_TASK_TYPE_NOT_FOUND"
|
|
90054
|
+
});
|
|
90055
|
+
return false;
|
|
90056
|
+
}
|
|
90057
|
+
async function verifySourceField(template, source, platform, errors4) {
|
|
90058
|
+
const storyTypes = template.filter.workItemTypes?.length ? template.filter.workItemTypes : [undefined];
|
|
90059
|
+
let found;
|
|
90060
|
+
for (const storyType of storyTypes) {
|
|
90061
|
+
const field = byReference(await platform.getFieldSchemas(storyType)).get(source);
|
|
90062
|
+
if (field) {
|
|
90063
|
+
found ??= field;
|
|
90064
|
+
continue;
|
|
90065
|
+
}
|
|
90066
|
+
errors4.push({
|
|
90067
|
+
path: "estimation.source",
|
|
90068
|
+
message: storyType ? `Field "${source}" not found for work item type "${storyType}".` : `Field "${source}" not found in this project.`,
|
|
90069
|
+
code: "ESTIMATION_SOURCE_NOT_FOUND"
|
|
90070
|
+
});
|
|
90071
|
+
}
|
|
90072
|
+
return found;
|
|
90073
|
+
}
|
|
90074
|
+
function checkSourceValues(template, field, warnings) {
|
|
90075
|
+
const table = template.estimation?.conversion?.table;
|
|
90076
|
+
const multipliers = template.estimation?.conversion?.multipliers === true;
|
|
90077
|
+
const allowed = field.isPicklist ? field.allowedValues ?? [] : [];
|
|
90078
|
+
if (table && allowed.length > 0) {
|
|
90079
|
+
const keys = Object.keys(table);
|
|
90080
|
+
const uncovered = allowed.filter((value) => lookupTableEstimate(value, table, multipliers) === undefined);
|
|
90081
|
+
if (uncovered.length > 0) {
|
|
90082
|
+
warnings.push({
|
|
90083
|
+
path: "estimation.conversion.table",
|
|
90084
|
+
message: `${listValues(uncovered)} ${uncovered.length === 1 ? "is a valid value" : "are valid values"} of "${field.referenceName}" but ${uncovered.length === 1 ? "has" : "have"} no conversion; Stories with ${uncovered.length === 1 ? "it" : "them"} will get blank Task Estimates.`,
|
|
90085
|
+
suggestion: "Add the missing values to conversion.table."
|
|
90086
|
+
});
|
|
90087
|
+
}
|
|
90088
|
+
const usedThroughMultiplier = (key) => multipliers && allowed.some((value) => value !== key && lookupTableEstimate(value, { [key]: 1 }, true) !== undefined);
|
|
90089
|
+
const unknown2 = keys.filter((key) => !allowed.includes(key) && !usedThroughMultiplier(key));
|
|
90090
|
+
if (unknown2.length > 0) {
|
|
90091
|
+
warnings.push({
|
|
90092
|
+
path: "estimation.conversion.table",
|
|
90093
|
+
message: `conversion.table ${unknown2.length === 1 ? "key" : "keys"} ${listValues(unknown2)} ${unknown2.length === 1 ? "is not an allowed value" : "are not allowed values"} of "${field.referenceName}" and will never match.`,
|
|
90094
|
+
suggestion: `Allowed values: ${listValues(allowed)}.`
|
|
90095
|
+
});
|
|
90096
|
+
}
|
|
90097
|
+
}
|
|
90098
|
+
if (field.type === "string" && field.allowsCustomValues) {
|
|
90099
|
+
const consequence = multipliers ? "any value that neither matches a conversion table key nor parses as a number followed by a key will leave Tasks unestimated." : "any value not in the conversion table will leave Tasks unestimated.";
|
|
90100
|
+
warnings.push({
|
|
90101
|
+
path: "estimation.source",
|
|
90102
|
+
message: field.isPicklist ? `"${field.referenceName}" is a suggested picklist that also accepts values outside its list; ${consequence}` : `"${field.referenceName}" is a free-text field; ${consequence}`
|
|
90103
|
+
});
|
|
90104
|
+
}
|
|
90105
|
+
if (field.type === "string") {
|
|
90106
|
+
template.tasks.forEach((task, i2) => {
|
|
90107
|
+
const conditions = [
|
|
90108
|
+
[`tasks[${i2}].condition`, task.condition],
|
|
90109
|
+
...(task.estimationPercentCondition ?? []).map((rule, j2) => [`tasks[${i2}].estimationPercentCondition[${j2}].condition`, rule.condition])
|
|
90110
|
+
];
|
|
90111
|
+
for (const [path, condition] of conditions) {
|
|
90112
|
+
if (condition && usesNumericOperatorOnEstimation(condition)) {
|
|
90113
|
+
warnings.push({
|
|
90114
|
+
path,
|
|
90115
|
+
message: `This condition compares estimation numerically, but "${field.referenceName}" holds text values; it will skip the task for any non-numeric Story Estimate.`,
|
|
90116
|
+
suggestion: "Use equals or not-equals with the category values instead."
|
|
90117
|
+
});
|
|
90118
|
+
}
|
|
90119
|
+
}
|
|
90120
|
+
});
|
|
90121
|
+
}
|
|
90122
|
+
}
|
|
90123
|
+
function usesNumericOperatorOnEstimation(condition) {
|
|
90124
|
+
if ("all" in condition)
|
|
90125
|
+
return condition.all.some(usesNumericOperatorOnEstimation);
|
|
90126
|
+
if ("any" in condition)
|
|
90127
|
+
return condition.any.some(usesNumericOperatorOnEstimation);
|
|
90128
|
+
return "field" in condition && condition.field === "estimation" && NUMERIC_OPERATORS2.has(condition.operator);
|
|
90129
|
+
}
|
|
90130
|
+
function byReference(fields) {
|
|
90131
|
+
return new Map(fields.map((field) => [field.referenceName, field]));
|
|
90132
|
+
}
|
|
90133
|
+
function listValues(values) {
|
|
90134
|
+
return values.map((value) => `"${value}"`).join(", ");
|
|
90135
|
+
}
|
|
90136
|
+
var NUMERIC_TYPES, NUMERIC_OPERATORS2;
|
|
90137
|
+
var init_estimation_verifier = __esm(() => {
|
|
90138
|
+
init_schema2();
|
|
90139
|
+
NUMERIC_TYPES = new Set(["integer", "decimal"]);
|
|
90140
|
+
NUMERIC_OPERATORS2 = new Set(["gt", "lt", "gte", "lte"]);
|
|
90141
|
+
});
|
|
90142
|
+
|
|
89592
90143
|
// ../atomize-core/src/templates/project-verifier.ts
|
|
89593
90144
|
function analyzeTemplateProjectVerification(template) {
|
|
89594
90145
|
const customFieldTaskCount = template.tasks.filter((task) => task.customFields && Object.keys(task.customFields).length > 0).length;
|
|
89595
90146
|
const conditionFieldRefs = Array.from(new Set(template.tasks.flatMap((task) => task.condition ? extractCustomFieldRefs(task.condition) : [])));
|
|
89596
90147
|
const hasSavedQuery = !!(template.filter?.savedQuery?.id || template.filter?.savedQuery?.path);
|
|
90148
|
+
const hasEstimationMapping = hasEstimationMappingOverrides(template);
|
|
89597
90149
|
return {
|
|
89598
90150
|
customFieldTaskCount,
|
|
89599
90151
|
conditionFieldRefs,
|
|
89600
90152
|
hasSavedQuery,
|
|
89601
|
-
|
|
90153
|
+
hasEstimationMapping,
|
|
90154
|
+
needsOnlineVerification: customFieldTaskCount > 0 || conditionFieldRefs.length > 0 || hasSavedQuery || hasEstimationMapping
|
|
89602
90155
|
};
|
|
89603
90156
|
}
|
|
89604
90157
|
async function verifyTemplateProject(template, options) {
|
|
@@ -89615,12 +90168,28 @@ async function verifyTemplateProject(template, options) {
|
|
|
89615
90168
|
return result;
|
|
89616
90169
|
}
|
|
89617
90170
|
if ((requirements.customFieldTaskCount > 0 || requirements.conditionFieldRefs.length > 0) && options.platform?.getFieldSchemas) {
|
|
89618
|
-
const customFields = await verifyTemplateCustomFields(template, options.platform.getFieldSchemas.bind(options.platform));
|
|
90171
|
+
const customFields = await verifyTemplateCustomFields(template, options.platform.getFieldSchemas.bind(options.platform), template.taskType ?? options.platform.getEstimationDefaults?.().taskWorkItemType);
|
|
89619
90172
|
result.errors.push(...customFields.errors);
|
|
89620
90173
|
result.warnings.push(...customFields.warnings);
|
|
89621
90174
|
if (customFields.errors.length > 0)
|
|
89622
90175
|
result.valid = false;
|
|
89623
90176
|
}
|
|
90177
|
+
const platform = options.platform;
|
|
90178
|
+
if (requirements.hasEstimationMapping && platform?.getFieldSchemas) {
|
|
90179
|
+
const estimation = await verifyEstimationMapping(template, {
|
|
90180
|
+
getFieldSchemas: platform.getFieldSchemas.bind(platform),
|
|
90181
|
+
getWorkItemTypes: platform.getWorkItemTypes?.bind(platform),
|
|
90182
|
+
getEstimationDefaults: platform.getEstimationDefaults?.bind(platform)
|
|
90183
|
+
});
|
|
90184
|
+
result.errors.push(...estimation.errors);
|
|
90185
|
+
if (options.strict === true || template.validation?.mode === "strict") {
|
|
90186
|
+
result.errors.push(...estimation.warnings.map((w2) => ({ path: w2.path, message: w2.message, suggestion: w2.suggestion, code: "STRICT_MODE_WARNING" })));
|
|
90187
|
+
} else {
|
|
90188
|
+
result.warnings.push(...estimation.warnings);
|
|
90189
|
+
}
|
|
90190
|
+
if (result.errors.length > 0)
|
|
90191
|
+
result.valid = false;
|
|
90192
|
+
}
|
|
89624
90193
|
if (requirements.hasSavedQuery && options.platform?.listSavedQueries) {
|
|
89625
90194
|
const savedQuery = await verifySavedQuery(template, options.platform);
|
|
89626
90195
|
result.errors.push(...savedQuery.errors);
|
|
@@ -89676,6 +90245,7 @@ async function verifySavedQuery(template, platform) {
|
|
|
89676
90245
|
var init_project_verifier = __esm(() => {
|
|
89677
90246
|
init_condition_evaluator();
|
|
89678
90247
|
init_custom_field_verifier();
|
|
90248
|
+
init_estimation_verifier();
|
|
89679
90249
|
});
|
|
89680
90250
|
|
|
89681
90251
|
// ../atomize-core/src/templates/template-verification.ts
|
|
@@ -92800,6 +93370,25 @@ function assertNotCancelled(value) {
|
|
|
92800
93370
|
}
|
|
92801
93371
|
return value;
|
|
92802
93372
|
}
|
|
93373
|
+
function parseConversionTable(input2) {
|
|
93374
|
+
const entries = input2.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
93375
|
+
if (entries.length === 0)
|
|
93376
|
+
return "Enter at least one entry, e.g. S=2, M=4, L=5";
|
|
93377
|
+
const table = {};
|
|
93378
|
+
for (const entry of entries) {
|
|
93379
|
+
const separator = entry.lastIndexOf("=");
|
|
93380
|
+
const key = separator > 0 ? entry.slice(0, separator).trim() : "";
|
|
93381
|
+
const value = Number(entry.slice(separator + 1).trim());
|
|
93382
|
+
if (!key || separator === entry.length - 1 || Number.isNaN(value))
|
|
93383
|
+
return `"${entry}" is not in the form value=amount`;
|
|
93384
|
+
if (value < 0)
|
|
93385
|
+
return `"${key}" cannot have a negative amount`;
|
|
93386
|
+
if (Object.hasOwn(table, key))
|
|
93387
|
+
return `"${key}" appears more than once`;
|
|
93388
|
+
table[key] = value;
|
|
93389
|
+
}
|
|
93390
|
+
return table;
|
|
93391
|
+
}
|
|
92803
93392
|
async function selectOrAutocomplete(opts) {
|
|
92804
93393
|
const count = opts.thresholdCount ?? opts.options.length;
|
|
92805
93394
|
if (count > AUTOCOMPLETE_THRESHOLD) {
|
|
@@ -92888,6 +93477,31 @@ var init_prompt_utilities = __esm(() => {
|
|
|
92888
93477
|
return required2;
|
|
92889
93478
|
return Validators.maxLength(fieldName, maxLength)(input2);
|
|
92890
93479
|
},
|
|
93480
|
+
positiveInteger: (fieldName) => (input2) => {
|
|
93481
|
+
const num = Number(input2);
|
|
93482
|
+
if (!input2 || input2.trim() === "" || !Number.isInteger(num) || num < 1)
|
|
93483
|
+
return `${fieldName} must be a whole number of at least 1`;
|
|
93484
|
+
return;
|
|
93485
|
+
},
|
|
93486
|
+
positiveNumber: (fieldName) => (input2) => {
|
|
93487
|
+
const num = Number(input2);
|
|
93488
|
+
if (!input2 || input2.trim() === "" || Number.isNaN(num))
|
|
93489
|
+
return `${fieldName} must be a number`;
|
|
93490
|
+
if (num <= 0)
|
|
93491
|
+
return `${fieldName} must be greater than 0`;
|
|
93492
|
+
return;
|
|
93493
|
+
},
|
|
93494
|
+
fieldReferenceName: (input2) => {
|
|
93495
|
+
if (!input2 || input2.trim() === "")
|
|
93496
|
+
return "Field reference name is required";
|
|
93497
|
+
if (/\s/.test(input2.trim()))
|
|
93498
|
+
return "Use the field's reference name (e.g. Custom.TShirtSize), which has no spaces";
|
|
93499
|
+
return;
|
|
93500
|
+
},
|
|
93501
|
+
conversionTable: (input2) => {
|
|
93502
|
+
const parsed = parseConversionTable(input2 ?? "");
|
|
93503
|
+
return typeof parsed === "string" ? parsed : undefined;
|
|
93504
|
+
},
|
|
92891
93505
|
estimationPercent: (input2) => {
|
|
92892
93506
|
const num = Number(input2);
|
|
92893
93507
|
if (Number.isNaN(num))
|
|
@@ -93201,9 +93815,19 @@ var init_profile_resolver = __esm(() => {
|
|
|
93201
93815
|
init_keychain_service();
|
|
93202
93816
|
});
|
|
93203
93817
|
|
|
93818
|
+
// ../atomize-core/src/services/template/estimation-grounding.ts
|
|
93819
|
+
function isEstimateCapableField(field, rules) {
|
|
93820
|
+
return isEstimateSourceField(field, rules);
|
|
93821
|
+
}
|
|
93822
|
+
var init_estimation_grounding = __esm(() => {
|
|
93823
|
+
init_src();
|
|
93824
|
+
init_src();
|
|
93825
|
+
});
|
|
93826
|
+
|
|
93204
93827
|
// src/cli/commands/template/template-wizard-helper.command.ts
|
|
93205
93828
|
var exports_template_wizard_helper_command = {};
|
|
93206
93829
|
__export(exports_template_wizard_helper_command, {
|
|
93830
|
+
renderConversionTable: () => renderConversionTable,
|
|
93207
93831
|
promptMixinRefs: () => promptMixinRefs,
|
|
93208
93832
|
configureValidation: () => configureValidation,
|
|
93209
93833
|
configureTemplateComposition: () => configureTemplateComposition,
|
|
@@ -93268,7 +93892,7 @@ async function configureFilter(ctx, defaults) {
|
|
|
93268
93892
|
initialValue: defaultFilterMode
|
|
93269
93893
|
}));
|
|
93270
93894
|
if (filterMode === "savedQuery") {
|
|
93271
|
-
return promptSavedQuery(ctx
|
|
93895
|
+
return promptSavedQuery(ctx, defaults);
|
|
93272
93896
|
}
|
|
93273
93897
|
const filter = {};
|
|
93274
93898
|
const workItemTypes = await promptWorkItemTypes(ctx, defaults?.workItemTypes);
|
|
@@ -93353,8 +93977,8 @@ async function configureFilter(ctx, defaults) {
|
|
|
93353
93977
|
}
|
|
93354
93978
|
return filter;
|
|
93355
93979
|
}
|
|
93356
|
-
async function promptSavedQuery(
|
|
93357
|
-
const sorted = [...
|
|
93980
|
+
async function promptSavedQuery(ctx, defaults) {
|
|
93981
|
+
const sorted = [...ctx.savedQueries].sort((a3, b3) => {
|
|
93358
93982
|
if (a3.isPublic !== b3.isPublic)
|
|
93359
93983
|
return a3.isPublic ? -1 : 1;
|
|
93360
93984
|
return a3.path.localeCompare(b3.path);
|
|
@@ -93368,16 +93992,21 @@ async function promptSavedQuery(queries) {
|
|
|
93368
93992
|
})),
|
|
93369
93993
|
placeholder: "Type to filter by path or name..."
|
|
93370
93994
|
});
|
|
93995
|
+
const workItemTypes = await promptWorkItemTypes(ctx, defaults?.workItemTypes, {
|
|
93996
|
+
message: "Which work item types does this query return? (used to look up their fields, not to filter)",
|
|
93997
|
+
required: true
|
|
93998
|
+
});
|
|
93371
93999
|
const excludeIfHasTasks = assertNotCancelled(await confirm({
|
|
93372
94000
|
message: "Exclude work items that already have tasks?",
|
|
93373
94001
|
initialValue: true
|
|
93374
94002
|
}));
|
|
93375
94003
|
return {
|
|
93376
94004
|
savedQuery: { id: selectedId.trim() },
|
|
94005
|
+
workItemTypes,
|
|
93377
94006
|
...excludeIfHasTasks && { excludeIfHasTasks: true }
|
|
93378
94007
|
};
|
|
93379
94008
|
}
|
|
93380
|
-
async function promptWorkItemTypes(ctx, defaults) {
|
|
94009
|
+
async function promptWorkItemTypes(ctx, defaults, options = {}) {
|
|
93381
94010
|
const defaultType = ctx.workItemTypes.includes("User Story") ? "User Story" : undefined;
|
|
93382
94011
|
const sorted = [
|
|
93383
94012
|
...defaultType ? [defaultType] : [],
|
|
@@ -93385,10 +94014,10 @@ async function promptWorkItemTypes(ctx, defaults) {
|
|
|
93385
94014
|
];
|
|
93386
94015
|
const initialValues = defaults && defaults.length > 0 ? defaults.filter((t3) => ctx.workItemTypes.includes(t3)) : defaultType ? [defaultType] : [];
|
|
93387
94016
|
const selected = assertNotCancelled(await multiselect({
|
|
93388
|
-
message: "Select work item types:",
|
|
94017
|
+
message: options.message ?? "Select work item types:",
|
|
93389
94018
|
options: sorted.map((t3) => ({ label: t3, value: t3 })),
|
|
93390
94019
|
initialValues,
|
|
93391
|
-
required: false
|
|
94020
|
+
required: options.required ?? false
|
|
93392
94021
|
}));
|
|
93393
94022
|
return selected;
|
|
93394
94023
|
}
|
|
@@ -93681,7 +94310,12 @@ async function promptDateFilter(selectMessage, customMessage, defaults) {
|
|
|
93681
94310
|
}
|
|
93682
94311
|
return choice;
|
|
93683
94312
|
}
|
|
93684
|
-
async function configureEstimation(defaults) {
|
|
94313
|
+
async function configureEstimation(defaults, ctx = {}, currentTaskType) {
|
|
94314
|
+
const source = await promptEstimationSource(defaults?.source, ctx);
|
|
94315
|
+
const sourceField = source ? ctx.storyFields?.find((field) => field.referenceName === source) : undefined;
|
|
94316
|
+
const conversion = await promptConversion(defaults?.conversion, sourceField);
|
|
94317
|
+
const sourceValues = sourceField?.isPicklist ? sourceField.allowedValues ?? [] : [];
|
|
94318
|
+
const { taskType, targetFields } = await promptEstimateTarget(currentTaskType, defaults?.targetFields, ctx);
|
|
93685
94319
|
const rounding = assertNotCancelled(await select({
|
|
93686
94320
|
message: "Rounding strategy:",
|
|
93687
94321
|
options: [
|
|
@@ -93692,23 +94326,267 @@ async function configureEstimation(defaults) {
|
|
|
93692
94326
|
],
|
|
93693
94327
|
initialValue: defaults?.rounding ?? "none"
|
|
93694
94328
|
}));
|
|
93695
|
-
const
|
|
93696
|
-
message: "Minimum
|
|
93697
|
-
initialValue: String(defaults?.minimumTaskPoints ?? 0),
|
|
93698
|
-
validate: (
|
|
93699
|
-
const n4 = Number(input2);
|
|
93700
|
-
if (Number.isNaN(n4))
|
|
93701
|
-
return "Must be a valid number";
|
|
93702
|
-
if (n4 < 0)
|
|
93703
|
-
return "Cannot be negative";
|
|
93704
|
-
return;
|
|
93705
|
-
}
|
|
94329
|
+
const minimumTaskEstimateRaw = assertNotCancelled(await text({
|
|
94330
|
+
message: "Minimum Task Estimate, in the Task's unit (0 for no minimum):",
|
|
94331
|
+
initialValue: String(defaults?.minimumTaskEstimate ?? defaults?.minimumTaskPoints ?? 0),
|
|
94332
|
+
validate: Validators.nonNegative("Minimum Task Estimate")
|
|
93706
94333
|
}));
|
|
93707
|
-
|
|
94334
|
+
const normalize = assertNotCancelled(await select({
|
|
94335
|
+
message: "When the generated tasks' percentages don't add up to 100%:",
|
|
94336
|
+
options: [
|
|
94337
|
+
{ label: "Fill up to 100% (scale up below 100%, keep anything above)", value: "auto" },
|
|
94338
|
+
{ label: "Always scale to exactly 100%", value: "always" },
|
|
94339
|
+
{ label: "Use the percentages as written", value: "never" }
|
|
94340
|
+
],
|
|
94341
|
+
initialValue: defaults?.normalize ?? "auto"
|
|
94342
|
+
}));
|
|
94343
|
+
const policy = assertNotCancelled(await select({
|
|
94344
|
+
message: "When a Story's estimate is missing or can't be converted:",
|
|
94345
|
+
options: [
|
|
94346
|
+
{ label: "Create the tasks with blank estimates and warn", value: "warn" },
|
|
94347
|
+
{ label: "Skip the Story", value: "skip" },
|
|
94348
|
+
{ label: "Assume a default Story Estimate", value: "use-default" }
|
|
94349
|
+
],
|
|
94350
|
+
initialValue: defaults?.ifParentHasNoEstimation ?? "warn"
|
|
94351
|
+
}));
|
|
94352
|
+
const defaultParentEstimation = policy === "use-default" ? await promptDefaultStoryEstimate(defaults?.defaultParentEstimation, conversion, sourceValues) : undefined;
|
|
94353
|
+
const {
|
|
94354
|
+
source: _source,
|
|
94355
|
+
conversion: _conversion,
|
|
94356
|
+
targetFields: _targetFields,
|
|
94357
|
+
minimumTaskPoints: _minimumTaskPoints,
|
|
94358
|
+
defaultParentEstimation: _default3,
|
|
94359
|
+
ifParentHasNoEstimation: _policy,
|
|
94360
|
+
normalize: _normalize2,
|
|
94361
|
+
...kept
|
|
94362
|
+
} = defaults ?? {};
|
|
94363
|
+
const estimation = {
|
|
94364
|
+
...kept,
|
|
93708
94365
|
strategy: "percentage",
|
|
93709
94366
|
rounding,
|
|
93710
|
-
|
|
93711
|
-
|
|
94367
|
+
minimumTaskEstimate: Number(minimumTaskEstimateRaw) || undefined,
|
|
94368
|
+
...source ? { source } : {},
|
|
94369
|
+
...conversion ? { conversion } : {},
|
|
94370
|
+
...targetFields?.length ? { targetFields } : {},
|
|
94371
|
+
...normalize !== "auto" ? { normalize } : {},
|
|
94372
|
+
...policy !== "warn" ? { ifParentHasNoEstimation: policy } : {},
|
|
94373
|
+
...defaultParentEstimation !== undefined ? { defaultParentEstimation } : {}
|
|
94374
|
+
};
|
|
94375
|
+
if (estimation.minimumTaskEstimate === undefined)
|
|
94376
|
+
delete estimation.minimumTaskEstimate;
|
|
94377
|
+
return { estimation, ...taskType ? { taskType } : {} };
|
|
94378
|
+
}
|
|
94379
|
+
async function promptEstimationSource(current, ctx) {
|
|
94380
|
+
const rules = { storyEstimateFields: ctx.defaults?.storyEstimateFields ?? [], nonEstimateFields: ctx.defaults?.nonEstimateFields };
|
|
94381
|
+
const candidates = sortEstimateFields((ctx.storyFields ?? []).filter((field) => isEstimateCapableField(field, rules)));
|
|
94382
|
+
if (ctx.storyFields !== undefined) {
|
|
94383
|
+
const choice = assertNotCancelled(await select({
|
|
94384
|
+
message: "Which Story field holds the Story Estimate?",
|
|
94385
|
+
options: [
|
|
94386
|
+
{ label: "Platform default (story points, effort or size)", value: "" },
|
|
94387
|
+
...candidates.map((field) => ({
|
|
94388
|
+
label: `${field.name} (${field.referenceName})`,
|
|
94389
|
+
value: field.referenceName,
|
|
94390
|
+
hint: field.isPicklist ? `values: ${(field.allowedValues ?? []).join(", ")}` : field.type
|
|
94391
|
+
})),
|
|
94392
|
+
{ label: "Another field (enter its reference name)", value: CUSTOM_ENTRY }
|
|
94393
|
+
],
|
|
94394
|
+
initialValue: current && candidates.some((field) => field.referenceName === current) ? current : current ? CUSTOM_ENTRY : ""
|
|
94395
|
+
}));
|
|
94396
|
+
if (choice !== CUSTOM_ENTRY)
|
|
94397
|
+
return choice || undefined;
|
|
94398
|
+
}
|
|
94399
|
+
const typed = assertNotCancelled(await text({
|
|
94400
|
+
message: "Story field reference name holding the Story Estimate (leave blank for the platform default):",
|
|
94401
|
+
initialValue: current ?? "",
|
|
94402
|
+
validate: (input2) => input2 && input2.trim() !== "" ? Validators.fieldReferenceName(input2) : undefined
|
|
94403
|
+
}));
|
|
94404
|
+
return typed.trim() || undefined;
|
|
94405
|
+
}
|
|
94406
|
+
async function promptConversion(current, sourceField) {
|
|
94407
|
+
const kind = assertNotCancelled(await select({
|
|
94408
|
+
message: "How does the Story Estimate translate into Task hours?",
|
|
94409
|
+
options: [
|
|
94410
|
+
{ label: "One-to-one (use the number as it is)", value: "none" },
|
|
94411
|
+
{ label: "A factor (e.g. 1 point = 4 hours)", value: "factor" },
|
|
94412
|
+
{ label: "A table (e.g. t-shirt sizes, or points that don't scale linearly)", value: "table" }
|
|
94413
|
+
],
|
|
94414
|
+
initialValue: current?.table ? "table" : current?.factor !== undefined ? "factor" : sourceField?.isPicklist ? "table" : "none"
|
|
94415
|
+
}));
|
|
94416
|
+
if (kind === "none")
|
|
94417
|
+
return;
|
|
94418
|
+
if (kind === "factor") {
|
|
94419
|
+
const factor = assertNotCancelled(await text({
|
|
94420
|
+
message: "Task hours per unit of Story Estimate:",
|
|
94421
|
+
initialValue: current?.factor !== undefined ? String(current.factor) : "",
|
|
94422
|
+
validate: Validators.positiveNumber("Factor")
|
|
94423
|
+
}));
|
|
94424
|
+
return { factor: Number(factor) };
|
|
94425
|
+
}
|
|
94426
|
+
const table = await promptConversionTable(current?.table, sourceField);
|
|
94427
|
+
const keysAreCategories = !Object.keys(table).every((key) => /^(?:\d+(?:\.\d+)?|\.\d+)$/.test(key));
|
|
94428
|
+
const multipliers = keysAreCategories && assertNotCancelled(await confirm({
|
|
94429
|
+
message: "Also accept multiples of a size, e.g. 0.3XL = 0.3 × XL?",
|
|
94430
|
+
initialValue: current?.multipliers === true
|
|
94431
|
+
}));
|
|
94432
|
+
return { table, ...multipliers ? { multipliers: true } : {} };
|
|
94433
|
+
}
|
|
94434
|
+
function renderConversionTable(table, source) {
|
|
94435
|
+
const allowed = source?.allowedValues ?? [];
|
|
94436
|
+
const entries = Object.entries(table);
|
|
94437
|
+
const heading = `Conversion table — ${source?.name ?? "Story Estimate"} → hours`;
|
|
94438
|
+
const lines = [allowed.length ? `${heading} ${allowed.filter((v2) => Object.hasOwn(table, v2)).length} of ${allowed.length} values covered` : heading];
|
|
94439
|
+
if (entries.length === 0) {
|
|
94440
|
+
lines.push(" (no values yet)");
|
|
94441
|
+
} else {
|
|
94442
|
+
const width = Math.max(5, ...entries.map(([key]) => key.length));
|
|
94443
|
+
lines.push(` ${"Value".padEnd(width)} ${"Hours".padEnd(6)} A 20% task gets`);
|
|
94444
|
+
for (const [key, hours] of entries) {
|
|
94445
|
+
const flag = allowed.length && !allowed.includes(key) ? " not in picklist" : "";
|
|
94446
|
+
lines.push(` ${key.padEnd(width)} ${String(hours).padEnd(6)} ${Math.round(hours * 20) / 100} h${flag}`);
|
|
94447
|
+
}
|
|
94448
|
+
}
|
|
94449
|
+
const missing = allowed.filter((value) => !Object.hasOwn(table, value));
|
|
94450
|
+
if (missing.length)
|
|
94451
|
+
lines.push(` Missing: ${missing.join(", ")} — Stories with ${missing.length === 1 ? "this value" : "these values"} get blank estimates.`);
|
|
94452
|
+
return lines;
|
|
94453
|
+
}
|
|
94454
|
+
async function promptConversionTable(current, sourceField) {
|
|
94455
|
+
const allowed = sourceField?.isPicklist ? sourceField.allowedValues ?? [] : [];
|
|
94456
|
+
const table = { ...current ?? {} };
|
|
94457
|
+
if (allowed.length && !current) {
|
|
94458
|
+
output3.print(source_default.gray(`Enter the Task hours for each ${sourceField?.name ?? "value"}. Leave blank to skip a value.`));
|
|
94459
|
+
for (const [index, value] of allowed.entries()) {
|
|
94460
|
+
const hours = assertNotCancelled(await text({
|
|
94461
|
+
message: `Hours for "${value}" (${index + 1} of ${allowed.length}):`,
|
|
94462
|
+
validate: (input2) => input2 && input2.trim() !== "" ? Validators.nonNegative("Hours")(input2) : undefined
|
|
94463
|
+
}));
|
|
94464
|
+
if (hours.trim() !== "")
|
|
94465
|
+
table[value] = Number(hours);
|
|
94466
|
+
}
|
|
94467
|
+
}
|
|
94468
|
+
for (;; ) {
|
|
94469
|
+
output3.blankLine();
|
|
94470
|
+
for (const line of renderConversionTable(table, sourceField))
|
|
94471
|
+
output3.print(source_default.gray(line));
|
|
94472
|
+
const missing = allowed.filter((value) => !Object.hasOwn(table, value));
|
|
94473
|
+
const keys = Object.keys(table);
|
|
94474
|
+
const action = assertNotCancelled(await select({
|
|
94475
|
+
message: "Conversion table",
|
|
94476
|
+
options: [
|
|
94477
|
+
...missing.map((value) => ({ label: `Add hours for ${value}`, value: `add:${value}` })),
|
|
94478
|
+
{ label: allowed.length ? "Add another value…" : "Add a value", value: "add" },
|
|
94479
|
+
...keys.length ? [{ label: "Change a value's hours", value: "change" }, { label: "Remove a value", value: "remove" }] : [],
|
|
94480
|
+
{ label: "Paste several (e.g. S=2, M=4)", value: "paste" },
|
|
94481
|
+
{ label: "Done", value: "done" }
|
|
94482
|
+
],
|
|
94483
|
+
initialValue: missing.length ? `add:${missing[0]}` : keys.length ? "done" : "add"
|
|
94484
|
+
}));
|
|
94485
|
+
if (action === "done") {
|
|
94486
|
+
if (keys.length)
|
|
94487
|
+
return table;
|
|
94488
|
+
output3.print(source_default.yellow("Add at least one value before finishing."));
|
|
94489
|
+
continue;
|
|
94490
|
+
}
|
|
94491
|
+
if (action.startsWith("add:")) {
|
|
94492
|
+
const value = action.slice(4);
|
|
94493
|
+
table[value] = Number(assertNotCancelled(await text({ message: `Hours for "${value}":`, validate: hoursValidator })));
|
|
94494
|
+
continue;
|
|
94495
|
+
}
|
|
94496
|
+
if (action === "add") {
|
|
94497
|
+
const value = assertNotCancelled(await text({
|
|
94498
|
+
message: "Story Estimate value (e.g. XL, or 8 for points):",
|
|
94499
|
+
validate: (input2) => {
|
|
94500
|
+
if (!input2 || input2.trim() === "")
|
|
94501
|
+
return "Enter a value";
|
|
94502
|
+
if (Object.hasOwn(table, input2.trim()))
|
|
94503
|
+
return `"${input2.trim()}" is already in the table`;
|
|
94504
|
+
return;
|
|
94505
|
+
}
|
|
94506
|
+
})).trim();
|
|
94507
|
+
table[value] = Number(assertNotCancelled(await text({ message: `Hours for "${value}":`, validate: hoursValidator })));
|
|
94508
|
+
continue;
|
|
94509
|
+
}
|
|
94510
|
+
if (action === "change") {
|
|
94511
|
+
const value = assertNotCancelled(await select({ message: "Which value?", options: keys.map((key) => ({ label: `${key} (${table[key]} h)`, value: key })) }));
|
|
94512
|
+
table[value] = Number(assertNotCancelled(await text({ message: `Hours for "${value}":`, initialValue: String(table[value]), validate: hoursValidator })));
|
|
94513
|
+
continue;
|
|
94514
|
+
}
|
|
94515
|
+
if (action === "remove") {
|
|
94516
|
+
const value = assertNotCancelled(await select({ message: "Remove which value?", options: keys.map((key) => ({ label: `${key} (${table[key]} h)`, value: key })) }));
|
|
94517
|
+
delete table[value];
|
|
94518
|
+
continue;
|
|
94519
|
+
}
|
|
94520
|
+
const pasted = assertNotCancelled(await text({ message: "Value=hours pairs, comma-separated (e.g. S=2, M=4, L=5):", validate: Validators.conversionTable }));
|
|
94521
|
+
Object.assign(table, parseConversionTable(pasted));
|
|
94522
|
+
}
|
|
94523
|
+
}
|
|
94524
|
+
async function promptEstimateTarget(currentTaskType, currentTargetFields, ctx) {
|
|
94525
|
+
const customise = assertNotCancelled(await confirm({
|
|
94526
|
+
message: `Create tasks as a different work item type, or write estimates to different fields? (default: ${ctx.defaults?.taskWorkItemType ?? "platform default"})`,
|
|
94527
|
+
initialValue: currentTaskType !== undefined || (currentTargetFields?.length ?? 0) > 0
|
|
94528
|
+
}));
|
|
94529
|
+
if (!customise)
|
|
94530
|
+
return {};
|
|
94531
|
+
const defaultType = ctx.defaults?.taskWorkItemType;
|
|
94532
|
+
let taskType;
|
|
94533
|
+
if (ctx.workItemTypes?.length) {
|
|
94534
|
+
taskType = assertNotCancelled(await select({
|
|
94535
|
+
message: "Work item type to create tasks as:",
|
|
94536
|
+
options: ctx.workItemTypes.map((type) => ({ label: type, value: type, hint: type === defaultType ? "platform default" : undefined })),
|
|
94537
|
+
initialValue: currentTaskType ?? defaultType ?? ctx.workItemTypes[0]
|
|
94538
|
+
}));
|
|
94539
|
+
} else {
|
|
94540
|
+
taskType = assertNotCancelled(await text({
|
|
94541
|
+
message: "Work item type to create tasks as:",
|
|
94542
|
+
initialValue: currentTaskType ?? defaultType ?? "",
|
|
94543
|
+
validate: Validators.required("Work item type")
|
|
94544
|
+
})).trim();
|
|
94545
|
+
}
|
|
94546
|
+
const numericFields = ctx.getTaskFields ? sortEstimateFields((await ctx.getTaskFields(taskType)).filter((field) => isEstimateTargetField(field, ctx.defaults))) : [];
|
|
94547
|
+
let targetFields;
|
|
94548
|
+
if (numericFields.length > 0) {
|
|
94549
|
+
targetFields = assertNotCancelled(await multiselect({
|
|
94550
|
+
message: "Task fields that receive the estimate (none selected = platform default fields):",
|
|
94551
|
+
options: numericFields.map((field) => ({ label: `${field.name} (${field.referenceName})`, value: field.referenceName })),
|
|
94552
|
+
initialValues: currentTargetFields ?? [],
|
|
94553
|
+
required: false
|
|
94554
|
+
}));
|
|
94555
|
+
} else {
|
|
94556
|
+
const typed = assertNotCancelled(await text({
|
|
94557
|
+
message: "Task field reference names that receive the estimate, comma-separated (blank = platform default fields):",
|
|
94558
|
+
initialValue: currentTargetFields?.join(", ") ?? ""
|
|
94559
|
+
}));
|
|
94560
|
+
targetFields = Filters.commaSeparated(typed).filter(Boolean);
|
|
94561
|
+
}
|
|
94562
|
+
const keepTaskType = taskType !== defaultType || currentTaskType !== undefined;
|
|
94563
|
+
return { ...keepTaskType ? { taskType } : {}, ...targetFields.length ? { targetFields } : {} };
|
|
94564
|
+
}
|
|
94565
|
+
async function promptDefaultStoryEstimate(current, conversion, sourceValues) {
|
|
94566
|
+
const choices = (sourceValues.length ? sourceValues : Object.keys(conversion?.table ?? {})).filter((value) => isConvertibleStoryEstimate(value, conversion));
|
|
94567
|
+
if (choices.length > 0) {
|
|
94568
|
+
const currentValue = current === undefined ? undefined : String(current);
|
|
94569
|
+
const choice = assertNotCancelled(await select({
|
|
94570
|
+
message: "Default Story Estimate to assume:",
|
|
94571
|
+
options: [
|
|
94572
|
+
...choices.map((value) => ({ label: value, value })),
|
|
94573
|
+
...conversion?.multipliers ? [{ label: "Another value (e.g. 0.5L)", value: CUSTOM_ENTRY }] : []
|
|
94574
|
+
],
|
|
94575
|
+
initialValue: currentValue && choices.includes(currentValue) ? currentValue : currentValue && conversion?.multipliers ? CUSTOM_ENTRY : choices[0]
|
|
94576
|
+
}));
|
|
94577
|
+
if (choice !== CUSTOM_ENTRY)
|
|
94578
|
+
return choice;
|
|
94579
|
+
}
|
|
94580
|
+
const typed = assertNotCancelled(await text({
|
|
94581
|
+
message: "Default Story Estimate to assume (e.g. 5, or a size like M):",
|
|
94582
|
+
initialValue: current !== undefined ? String(current) : "",
|
|
94583
|
+
validate: (input2) => {
|
|
94584
|
+
if (!input2 || input2.trim() === "")
|
|
94585
|
+
return "A default Story Estimate is required";
|
|
94586
|
+
return isConvertibleStoryEstimate(input2.trim(), conversion) ? undefined : conversion?.table ? "Must be a value of the conversion table, or a multiple of one" : "Must be a number unless a conversion table is set";
|
|
94587
|
+
}
|
|
94588
|
+
})).trim();
|
|
94589
|
+
return Number.isFinite(Number(typed)) ? Number(typed) : typed;
|
|
93712
94590
|
}
|
|
93713
94591
|
async function configureValidation(defaults) {
|
|
93714
94592
|
const validation = {};
|
|
@@ -93953,9 +94831,15 @@ async function promptMixinRefs(mixins) {
|
|
|
93953
94831
|
function formatCatalogChoice(item) {
|
|
93954
94832
|
return `${item.displayName} (${item.scope})`;
|
|
93955
94833
|
}
|
|
93956
|
-
var output3, ADVANCED_GROUP_ORDER, DATE_FILTER_OPTIONS
|
|
94834
|
+
var output3, ADVANCED_GROUP_ORDER, DATE_FILTER_OPTIONS, CUSTOM_ENTRY = "__custom__", hoursValidator = (input2) => {
|
|
94835
|
+
if (!input2 || input2.trim() === "")
|
|
94836
|
+
return "Enter the hours";
|
|
94837
|
+
return Validators.nonNegative("Hours")(input2);
|
|
94838
|
+
};
|
|
93957
94839
|
var init_template_wizard_helper_command = __esm(() => {
|
|
93958
94840
|
init_dist6();
|
|
94841
|
+
init_estimation_grounding();
|
|
94842
|
+
init_schema2();
|
|
93959
94843
|
init_source();
|
|
93960
94844
|
init_command_output();
|
|
93961
94845
|
init_prompt_utilities();
|
|
@@ -97894,7 +98778,7 @@ var program = new Command;
|
|
|
97894
98778
|
|
|
97895
98779
|
// package.json
|
|
97896
98780
|
var name = "@sppg2001/atomize";
|
|
97897
|
-
var version2 = "4.0
|
|
98781
|
+
var version2 = "4.2.0";
|
|
97898
98782
|
|
|
97899
98783
|
// src/cli/commands/auth/auth-add.command.ts
|
|
97900
98784
|
init_dist6();
|
|
@@ -98850,6 +99734,7 @@ init_dist6();
|
|
|
98850
99734
|
init_logger2();
|
|
98851
99735
|
init_estimation_distribution();
|
|
98852
99736
|
init_platform_factory();
|
|
99737
|
+
init_schema2();
|
|
98853
99738
|
init_template_library();
|
|
98854
99739
|
init_errors();
|
|
98855
99740
|
|
|
@@ -99396,16 +100281,26 @@ function printReport(report, options, dryRun, storyIds) {
|
|
|
99396
100281
|
`));
|
|
99397
100282
|
for (const result of report.results) {
|
|
99398
100283
|
if (result.success) {
|
|
100284
|
+
if (result.skipReason) {
|
|
100285
|
+
output2.print(source_default.yellow(`⊘ ${sanitizeTty(result.story.id)}: ${sanitizeTty(result.story.title)}`));
|
|
100286
|
+
output2.print(source_default.gray(` Skipped: ${sanitizeTty(result.skipReason)}`));
|
|
100287
|
+
continue;
|
|
100288
|
+
}
|
|
99399
100289
|
output2.print(source_default.green(`✓ ${sanitizeTty(result.story.id)}: ${sanitizeTty(result.story.title)}`));
|
|
99400
|
-
|
|
100290
|
+
const withUnit = (value) => result.estimateUnit ? `${formatEstimateValue(value)} ${result.estimateUnit}` : formatEstimateValue(value);
|
|
100291
|
+
const rawEstimate = result.storyEstimate;
|
|
100292
|
+
const converted = result.estimationSummary?.storyEstimation;
|
|
100293
|
+
const conversionNote = converted !== undefined && converted !== rawEstimate ? ` (converted to ${withUnit(converted)})` : "";
|
|
100294
|
+
output2.print(source_default.gray(` Estimation: ${rawEstimate === undefined ? "none" : sanitizeTty(String(rawEstimate))}${conversionNote}`));
|
|
99401
100295
|
output2.print(source_default.gray(` Tasks: ${result.tasksCalculated.length}`));
|
|
99402
100296
|
if (result.estimationSummary) {
|
|
99403
|
-
output2.print(source_default.gray(` Distribution: ${result.estimationSummary.totalTaskEstimation}
|
|
100297
|
+
output2.print(source_default.gray(` Distribution: ${withUnit(result.estimationSummary.totalTaskEstimation)} (${result.estimationSummary.percentageUsed.toFixed(0)}%)`));
|
|
99404
100298
|
}
|
|
99405
100299
|
if ((options.verbose || dryRun) && result.tasksCalculated.length > 0) {
|
|
99406
100300
|
output2.print(source_default.gray(" Task breakdown:"));
|
|
99407
100301
|
for (const task of result.tasksCalculated) {
|
|
99408
|
-
|
|
100302
|
+
const estimate = task.estimation === undefined ? "unestimated" : withUnit(task.estimation);
|
|
100303
|
+
output2.print(source_default.gray(` - ${sanitizeTty(task.title)}: ${estimate} (${task.estimationPercent}%)`));
|
|
99409
100304
|
}
|
|
99410
100305
|
}
|
|
99411
100306
|
} else {
|
|
@@ -99577,6 +100472,8 @@ function makeGenerateCommand(makeOutput, prompts, config2) {
|
|
|
99577
100472
|
});
|
|
99578
100473
|
}
|
|
99579
100474
|
async function resolveNormalization(template, isTTYSession, prompts) {
|
|
100475
|
+
if (template.estimation?.normalize && template.estimation.normalize !== "auto")
|
|
100476
|
+
return false;
|
|
99580
100477
|
const { shouldOffer, total } = shouldOfferOverageNormalization(template.tasks);
|
|
99581
100478
|
if (!shouldOffer)
|
|
99582
100479
|
return false;
|
|
@@ -99594,6 +100491,7 @@ async function resolveNormalization(template, isTTYSession, prompts) {
|
|
|
99594
100491
|
init_condition_evaluator();
|
|
99595
100492
|
init_dependency_resolver();
|
|
99596
100493
|
init_estimation_calculator();
|
|
100494
|
+
init_mock_adapter();
|
|
99597
100495
|
var STANDARD_FIELD_TYPES = {
|
|
99598
100496
|
id: "string",
|
|
99599
100497
|
title: "string",
|
|
@@ -99652,7 +100550,11 @@ function inspectTemplate(template) {
|
|
|
99652
100550
|
}
|
|
99653
100551
|
}
|
|
99654
100552
|
if (template.tasks.some((task) => task.estimationPercent !== undefined || task.estimationPercentCondition?.length)) {
|
|
99655
|
-
|
|
100553
|
+
const source = template.estimation?.source;
|
|
100554
|
+
if (source)
|
|
100555
|
+
upsert(source, "unknown", "estimation", { required: true });
|
|
100556
|
+
else
|
|
100557
|
+
upsert("estimation", "number", "estimation", { required: true });
|
|
99656
100558
|
}
|
|
99657
100559
|
for (const name2 of filterFields) {
|
|
99658
100560
|
upsert(name2, STANDARD_FIELD_TYPES[name2] ?? "unknown", "filter");
|
|
@@ -99695,13 +100597,16 @@ function runPreview(template, mockStoryJson) {
|
|
|
99695
100597
|
const story = parseMockStory(mockStoryJson);
|
|
99696
100598
|
const orderedTasks = new DependencyResolver().resolveDependencies(template.tasks);
|
|
99697
100599
|
const calc = new EstimationCalculator;
|
|
99698
|
-
const
|
|
99699
|
-
|
|
100600
|
+
const fieldMapping = resolveEstimationFieldMapping(MOCK_ESTIMATION_DEFAULTS, {
|
|
100601
|
+
source: template.estimation?.source,
|
|
100602
|
+
targetFields: template.estimation?.targetFields
|
|
100603
|
+
});
|
|
100604
|
+
const { calculatedTasks, skippedTasks, unresolvedEstimate } = calc.calculateTasksWithSkipped(story, "", orderedTasks, template.estimation, false, fieldMapping);
|
|
100605
|
+
const summary = calc.getEstimationSummary(story, calculatedTasks, template.estimation, fieldMapping);
|
|
99700
100606
|
const tasks = calculatedTasks.map((task) => {
|
|
99701
|
-
const result = {
|
|
99702
|
-
|
|
99703
|
-
estimation
|
|
99704
|
-
};
|
|
100607
|
+
const result = { title: task.title };
|
|
100608
|
+
if (task.estimation !== undefined)
|
|
100609
|
+
result.estimation = task.estimation;
|
|
99705
100610
|
if (task.estimationPercent !== undefined)
|
|
99706
100611
|
result.estimationPercent = task.estimationPercent;
|
|
99707
100612
|
if (task.dependsOn?.length)
|
|
@@ -99720,6 +100625,8 @@ function runPreview(template, mockStoryJson) {
|
|
|
99720
100625
|
title: s2.templateTask.title,
|
|
99721
100626
|
reason: s2.reason
|
|
99722
100627
|
})),
|
|
100628
|
+
...unresolvedEstimate ? { unresolvedEstimate } : {},
|
|
100629
|
+
...fieldMapping.unitLabel ? { estimateUnit: fieldMapping.unitLabel } : {},
|
|
99723
100630
|
estimationSummary: {
|
|
99724
100631
|
storyEstimation: summary.storyEstimation,
|
|
99725
100632
|
totalTaskEstimation: summary.totalTaskEstimation,
|
|
@@ -100041,6 +100948,70 @@ init_errors();
|
|
|
100041
100948
|
// ../atomize-core/src/services/template/learned-template-product.ts
|
|
100042
100949
|
init_estimation_distribution();
|
|
100043
100950
|
|
|
100951
|
+
// ../atomize-core/src/core/estimation-inference.ts
|
|
100952
|
+
function taskEstimateShares(taskEstimates) {
|
|
100953
|
+
const total = taskEstimates.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
100954
|
+
if (total <= 0)
|
|
100955
|
+
return taskEstimates.map(() => 0);
|
|
100956
|
+
return taskEstimates.map((value) => Math.round((value ?? 0) / total * 100));
|
|
100957
|
+
}
|
|
100958
|
+
function inferEstimation(samples) {
|
|
100959
|
+
const usable = samples.map((sample) => ({ estimate: sample.storyEstimate, total: sum(sample.taskEstimates) })).filter((s2) => s2.estimate !== undefined && s2.total > 0);
|
|
100960
|
+
if (usable.length === 0)
|
|
100961
|
+
return { lowConfidenceValues: [] };
|
|
100962
|
+
const defaultStoryEstimate = mostCommon(usable.map((s2) => s2.estimate));
|
|
100963
|
+
const allNumeric = usable.every((s2) => numericStoryEstimate(s2.estimate) !== undefined);
|
|
100964
|
+
if (allNumeric) {
|
|
100965
|
+
const ratios = usable.map((s2) => ({ points: numericStoryEstimate(s2.estimate) ?? 0, total: s2.total })).filter((s2) => s2.points > 0).map((s2) => s2.total / s2.points);
|
|
100966
|
+
if (ratios.length === 0)
|
|
100967
|
+
return { defaultStoryEstimate, lowConfidenceValues: [] };
|
|
100968
|
+
const factor = roundTo(median(ratios), 2);
|
|
100969
|
+
return {
|
|
100970
|
+
...factor !== 1 ? { conversion: { factor } } : {},
|
|
100971
|
+
defaultStoryEstimate,
|
|
100972
|
+
lowConfidenceValues: []
|
|
100973
|
+
};
|
|
100974
|
+
}
|
|
100975
|
+
const totalsByValue = new Map;
|
|
100976
|
+
for (const s2 of usable) {
|
|
100977
|
+
const key = String(s2.estimate).trim();
|
|
100978
|
+
totalsByValue.set(key, [...totalsByValue.get(key) ?? [], s2.total]);
|
|
100979
|
+
}
|
|
100980
|
+
const table = {};
|
|
100981
|
+
const lowConfidenceValues = [];
|
|
100982
|
+
for (const [key, totals] of totalsByValue) {
|
|
100983
|
+
table[key] = roundTo(median(totals), 2);
|
|
100984
|
+
if (totals.length === 1)
|
|
100985
|
+
lowConfidenceValues.push(key);
|
|
100986
|
+
}
|
|
100987
|
+
return { conversion: { table }, defaultStoryEstimate, lowConfidenceValues };
|
|
100988
|
+
}
|
|
100989
|
+
function sum(values) {
|
|
100990
|
+
return values.reduce((total, value) => total + (value ?? 0), 0);
|
|
100991
|
+
}
|
|
100992
|
+
function median(values) {
|
|
100993
|
+
const sorted = [...values].sort((a3, b3) => a3 - b3);
|
|
100994
|
+
const mid = Math.floor(sorted.length / 2);
|
|
100995
|
+
return sorted.length % 2 === 0 ? ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2 : sorted[mid] ?? 0;
|
|
100996
|
+
}
|
|
100997
|
+
function mostCommon(values) {
|
|
100998
|
+
const counts = new Map;
|
|
100999
|
+
for (const value of values)
|
|
101000
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
101001
|
+
let best;
|
|
101002
|
+
let bestCount = 0;
|
|
101003
|
+
for (const [value, count] of counts) {
|
|
101004
|
+
if (count > bestCount) {
|
|
101005
|
+
best = value;
|
|
101006
|
+
bestCount = count;
|
|
101007
|
+
}
|
|
101008
|
+
}
|
|
101009
|
+
return best;
|
|
101010
|
+
}
|
|
101011
|
+
function roundTo(value, decimals) {
|
|
101012
|
+
const scale = 10 ** decimals;
|
|
101013
|
+
return Math.round(value * scale) / scale;
|
|
101014
|
+
}
|
|
100044
101015
|
// ../atomize-core/src/services/template/pattern-detection.ts
|
|
100045
101016
|
init_dist();
|
|
100046
101017
|
var DEFAULT_PATTERN_SCORING_CONFIG = {
|
|
@@ -100631,12 +101602,12 @@ class ConditionPatternDetector {
|
|
|
100631
101602
|
}
|
|
100632
101603
|
detectEstimationConditions(analyses, commonTasks, threshold) {
|
|
100633
101604
|
const patterns = [];
|
|
100634
|
-
const estimations = analyses.map((a3) => a3.story.estimation).filter((e2) => e2 !== undefined && e2 > 0).sort((a3, b3) => a3 - b3);
|
|
101605
|
+
const estimations = analyses.map((a3) => numericStoryEstimate(a3.story.estimation)).filter((e2) => e2 !== undefined && e2 > 0).sort((a3, b3) => a3 - b3);
|
|
100635
101606
|
if (estimations.length < this.minSampleSize)
|
|
100636
101607
|
return patterns;
|
|
100637
|
-
const
|
|
100638
|
-
const q75 = estimations[Math.floor(estimations.length * 3 / 4)] ??
|
|
100639
|
-
const thresholds = [...new Set([
|
|
101608
|
+
const median2 = estimations[Math.floor(estimations.length / 2)] ?? 0;
|
|
101609
|
+
const q75 = estimations[Math.floor(estimations.length * 3 / 4)] ?? median2;
|
|
101610
|
+
const thresholds = [...new Set([median2, q75])].filter((t3) => t3 > 0);
|
|
100640
101611
|
for (const commonTask of commonTasks) {
|
|
100641
101612
|
if (commonTask.frequencyRatio >= 0.9)
|
|
100642
101613
|
continue;
|
|
@@ -100666,7 +101637,7 @@ class ConditionPatternDetector {
|
|
|
100666
101637
|
let taskInLargeStories = 0;
|
|
100667
101638
|
let taskInSmallStories = 0;
|
|
100668
101639
|
for (const analysis of analyses) {
|
|
100669
|
-
const estimation = analysis.story.estimation;
|
|
101640
|
+
const estimation = numericStoryEstimate(analysis.story.estimation);
|
|
100670
101641
|
if (estimation === undefined)
|
|
100671
101642
|
continue;
|
|
100672
101643
|
const isLarge = estimation >= estimationThreshold;
|
|
@@ -100779,7 +101750,7 @@ class ConditionPatternDetector {
|
|
|
100779
101750
|
return false;
|
|
100780
101751
|
}
|
|
100781
101752
|
case "estimation": {
|
|
100782
|
-
const e2 = story.estimation;
|
|
101753
|
+
const e2 = numericStoryEstimate(story.estimation);
|
|
100783
101754
|
if (e2 === undefined)
|
|
100784
101755
|
return false;
|
|
100785
101756
|
const v2 = condition.value;
|
|
@@ -100950,18 +101921,18 @@ class FilterLearner {
|
|
|
100950
101921
|
for (const p3 of priorities) {
|
|
100951
101922
|
priorityCounts.set(p3, (priorityCounts.get(p3) ?? 0) + 1);
|
|
100952
101923
|
}
|
|
100953
|
-
let
|
|
101924
|
+
let mostCommon2 = min;
|
|
100954
101925
|
let maxCount = 0;
|
|
100955
101926
|
for (const [priority, count] of priorityCounts) {
|
|
100956
101927
|
if (count > maxCount) {
|
|
100957
|
-
|
|
101928
|
+
mostCommon2 = priority;
|
|
100958
101929
|
maxCount = count;
|
|
100959
101930
|
}
|
|
100960
101931
|
}
|
|
100961
|
-
return { min, max, mostCommon };
|
|
101932
|
+
return { min, max, mostCommon: mostCommon2 };
|
|
100962
101933
|
}
|
|
100963
101934
|
detectEstimationRange(analyses) {
|
|
100964
|
-
const estimations = analyses.map((a3) => a3.story.estimation).filter((e2) => e2 !== undefined && e2 > 0);
|
|
101935
|
+
const estimations = analyses.map((a3) => numericStoryEstimate(a3.story.estimation)).filter((e2) => e2 !== undefined && e2 > 0);
|
|
100965
101936
|
if (estimations.length === 0)
|
|
100966
101937
|
return;
|
|
100967
101938
|
const sorted = [...estimations].sort((a3, b3) => a3 - b3);
|
|
@@ -101042,7 +102013,7 @@ class FilterLearner {
|
|
|
101042
102013
|
if (max - min <= 3) {
|
|
101043
102014
|
suggestions.push({
|
|
101044
102015
|
type: "improve-filter",
|
|
101045
|
-
message: `Stories have similar estimations (${min}-${max}
|
|
102016
|
+
message: `Stories have similar estimations (${min}-${max}, avg ${average}). This template works well for stories of this size.`,
|
|
101046
102017
|
severity: "info"
|
|
101047
102018
|
});
|
|
101048
102019
|
}
|
|
@@ -101065,8 +102036,8 @@ class FilterLearner {
|
|
|
101065
102036
|
parts.push(`Area paths: ${learnedFilters.areaPaths.values.join(", ")}`);
|
|
101066
102037
|
}
|
|
101067
102038
|
if (learnedFilters.priorityRange) {
|
|
101068
|
-
const { min, max, mostCommon } = learnedFilters.priorityRange;
|
|
101069
|
-
parts.push(`Priority range: ${min}-${max} (most common: ${
|
|
102039
|
+
const { min, max, mostCommon: mostCommon2 } = learnedFilters.priorityRange;
|
|
102040
|
+
parts.push(`Priority range: ${min}-${max} (most common: ${mostCommon2})`);
|
|
101070
102041
|
}
|
|
101071
102042
|
if (learnedFilters.estimationRange) {
|
|
101072
102043
|
const { min, max, average } = learnedFilters.estimationRange;
|
|
@@ -101215,7 +102186,7 @@ class PatternDetector extends SimilarityCalculator {
|
|
|
101215
102186
|
detectEstimationPattern(analyses) {
|
|
101216
102187
|
const totalEstimations = [];
|
|
101217
102188
|
for (const analysis of analyses) {
|
|
101218
|
-
const storyEstimation = analysis.story.estimation ?? 0;
|
|
102189
|
+
const storyEstimation = numericStoryEstimate(analysis.story.estimation) ?? 0;
|
|
101219
102190
|
totalEstimations.push(storyEstimation);
|
|
101220
102191
|
}
|
|
101221
102192
|
const avgTotal = totalEstimations.length > 0 ? totalEstimations.reduce((a3, b3) => a3 + b3, 0) / totalEstimations.length : 0;
|
|
@@ -101297,7 +102268,7 @@ class ConfidenceScorer {
|
|
|
101297
102268
|
};
|
|
101298
102269
|
}
|
|
101299
102270
|
scoreTaskConsistency(analyses, patterns) {
|
|
101300
|
-
const totalTasks = analyses.reduce((
|
|
102271
|
+
const totalTasks = analyses.reduce((sum2, a3) => sum2 + a3.tasks.length, 0);
|
|
101301
102272
|
if (totalTasks === 0) {
|
|
101302
102273
|
return {
|
|
101303
102274
|
name: "Task Consistency",
|
|
@@ -101377,9 +102348,9 @@ class ConfidenceScorer {
|
|
|
101377
102348
|
description: "No tasks to merge"
|
|
101378
102349
|
};
|
|
101379
102350
|
}
|
|
101380
|
-
const totalOriginalTasks = analyses.reduce((
|
|
102351
|
+
const totalOriginalTasks = analyses.reduce((sum2, a3) => sum2 + a3.tasks.length, 0);
|
|
101381
102352
|
const mergeRatio = totalOriginalTasks > 0 ? 1 - mergedTasks.length / totalOriginalTasks : 0;
|
|
101382
|
-
const avgSimilarity = mergedTasks.reduce((
|
|
102353
|
+
const avgSimilarity = mergedTasks.reduce((sum2, m3) => sum2 + m3.similarity, 0) / mergedTasks.length;
|
|
101383
102354
|
const compositeScore = mergeRatio * 0.6 + avgSimilarity * 0.4;
|
|
101384
102355
|
const score = Math.round(compositeScore * 100);
|
|
101385
102356
|
return {
|
|
@@ -101418,7 +102389,7 @@ class ConfidenceScorer {
|
|
|
101418
102389
|
description: "No dependency patterns detected"
|
|
101419
102390
|
};
|
|
101420
102391
|
}
|
|
101421
|
-
const avgConfidence = dependencyPatterns.reduce((
|
|
102392
|
+
const avgConfidence = dependencyPatterns.reduce((sum2, d2) => sum2 + d2.confidence, 0) / dependencyPatterns.length;
|
|
101422
102393
|
const explicitCount = dependencyPatterns.filter((d2) => d2.source === "explicit").length;
|
|
101423
102394
|
const explicitRatio = explicitCount / dependencyPatterns.length;
|
|
101424
102395
|
const explicitBonus = explicitRatio * 10;
|
|
@@ -101440,8 +102411,8 @@ class ConfidenceScorer {
|
|
|
101440
102411
|
description: "No conditional patterns detected"
|
|
101441
102412
|
};
|
|
101442
102413
|
}
|
|
101443
|
-
const avgConfidence = conditionalPatterns.reduce((
|
|
101444
|
-
const avgCoverage = conditionalPatterns.reduce((
|
|
102414
|
+
const avgConfidence = conditionalPatterns.reduce((sum2, c4) => sum2 + c4.confidence, 0) / conditionalPatterns.length;
|
|
102415
|
+
const avgCoverage = conditionalPatterns.reduce((sum2, c4) => sum2 + (c4.totalStories > 0 ? c4.matchCount / c4.totalStories : 0), 0) / conditionalPatterns.length;
|
|
101445
102416
|
const score = Math.round(Math.min(100, avgConfidence * 70 + avgCoverage * 30));
|
|
101446
102417
|
return {
|
|
101447
102418
|
name: "Condition Quality",
|
|
@@ -101451,7 +102422,7 @@ class ConfidenceScorer {
|
|
|
101451
102422
|
};
|
|
101452
102423
|
}
|
|
101453
102424
|
calculateOverall(factors, sampleCount) {
|
|
101454
|
-
const baseScore = factors.reduce((
|
|
102425
|
+
const baseScore = factors.reduce((sum2, f2) => sum2 + f2.score * f2.weight, 0);
|
|
101455
102426
|
const extraWeightMap = {
|
|
101456
102427
|
1: 0.5,
|
|
101457
102428
|
2: 0.25,
|
|
@@ -101505,35 +102476,35 @@ class OutlierDetector {
|
|
|
101505
102476
|
const sorted = [...values].sort((a3, b3) => a3 - b3);
|
|
101506
102477
|
const n4 = sorted.length;
|
|
101507
102478
|
const midIndex = Math.floor(n4 / 2);
|
|
101508
|
-
const
|
|
101509
|
-
const deviations = values.map((v2) => Math.abs(v2 -
|
|
102479
|
+
const median2 = n4 % 2 === 1 ? sorted[midIndex] ?? 0 : ((sorted[midIndex - 1] ?? 0) + (sorted[midIndex] ?? 0)) / 2;
|
|
102480
|
+
const deviations = values.map((v2) => Math.abs(v2 - median2));
|
|
101510
102481
|
const sortedDev = [...deviations].sort((a3, b3) => a3 - b3);
|
|
101511
102482
|
const mad = n4 % 2 === 1 ? sortedDev[midIndex] ?? 0 : ((sortedDev[midIndex - 1] ?? 0) + (sortedDev[midIndex] ?? 0)) / 2;
|
|
101512
|
-
return { median, mad };
|
|
102483
|
+
return { median: median2, mad };
|
|
101513
102484
|
}
|
|
101514
|
-
modifiedZScore(value,
|
|
102485
|
+
modifiedZScore(value, median2, mad) {
|
|
101515
102486
|
if (mad === 0)
|
|
101516
102487
|
return 0;
|
|
101517
|
-
return this.madToStdDev * (value -
|
|
102488
|
+
return this.madToStdDev * (value - median2) / mad;
|
|
101518
102489
|
}
|
|
101519
102490
|
detectEstimationOutliers(analyses) {
|
|
101520
102491
|
const estimations = analyses.map((a3) => ({
|
|
101521
102492
|
storyId: a3.story.id,
|
|
101522
|
-
estimation: a3.story.estimation ?? 0
|
|
102493
|
+
estimation: numericStoryEstimate(a3.story.estimation) ?? 0
|
|
101523
102494
|
})).filter((e2) => e2.estimation > 0);
|
|
101524
102495
|
if (estimations.length < 2)
|
|
101525
102496
|
return [];
|
|
101526
102497
|
const values = estimations.map((e2) => e2.estimation);
|
|
101527
|
-
const { median, mad } = this.calculateMAD(values);
|
|
102498
|
+
const { median: median2, mad } = this.calculateMAD(values);
|
|
101528
102499
|
if (mad === 0)
|
|
101529
102500
|
return [];
|
|
101530
102501
|
const outliers = [];
|
|
101531
102502
|
for (const e2 of estimations) {
|
|
101532
|
-
const zScore = this.modifiedZScore(e2.estimation,
|
|
102503
|
+
const zScore = this.modifiedZScore(e2.estimation, median2, mad);
|
|
101533
102504
|
const absZ = Math.abs(zScore);
|
|
101534
102505
|
if (absZ > this.zThreshold) {
|
|
101535
|
-
const expectedLower =
|
|
101536
|
-
const expectedUpper =
|
|
102506
|
+
const expectedLower = median2 - this.zThreshold * mad / this.madToStdDev;
|
|
102507
|
+
const expectedUpper = median2 + this.zThreshold * mad / this.madToStdDev;
|
|
101537
102508
|
outliers.push({
|
|
101538
102509
|
type: "estimation",
|
|
101539
102510
|
storyId: e2.storyId,
|
|
@@ -101557,16 +102528,16 @@ class OutlierDetector {
|
|
|
101557
102528
|
if (counts.length < 2)
|
|
101558
102529
|
return [];
|
|
101559
102530
|
const values = counts.map((c4) => c4.count);
|
|
101560
|
-
const { median, mad } = this.calculateMAD(values);
|
|
102531
|
+
const { median: median2, mad } = this.calculateMAD(values);
|
|
101561
102532
|
if (mad === 0)
|
|
101562
102533
|
return [];
|
|
101563
102534
|
const outliers = [];
|
|
101564
102535
|
for (const c4 of counts) {
|
|
101565
|
-
const zScore = this.modifiedZScore(c4.count,
|
|
102536
|
+
const zScore = this.modifiedZScore(c4.count, median2, mad);
|
|
101566
102537
|
const absZ = Math.abs(zScore);
|
|
101567
102538
|
if (absZ > this.zThreshold) {
|
|
101568
|
-
const expectedLower =
|
|
101569
|
-
const expectedUpper =
|
|
102539
|
+
const expectedLower = median2 - this.zThreshold * mad / this.madToStdDev;
|
|
102540
|
+
const expectedUpper = median2 + this.zThreshold * mad / this.madToStdDev;
|
|
101570
102541
|
outliers.push({
|
|
101571
102542
|
type: "task-count",
|
|
101572
102543
|
storyId: c4.storyId,
|
|
@@ -101626,7 +102597,7 @@ class OutlierDetector {
|
|
|
101626
102597
|
|
|
101627
102598
|
// ../atomize-core/src/services/template/learned-template-product.ts
|
|
101628
102599
|
class LearnedTemplateProductBuilder {
|
|
101629
|
-
buildMergedTemplate(analyses, mergedTasks) {
|
|
102600
|
+
buildMergedTemplate(analyses, mergedTasks, estimationSource) {
|
|
101630
102601
|
const taskDefinitions = mergedTasks.map((mt) => {
|
|
101631
102602
|
const task = { ...mt.task };
|
|
101632
102603
|
if (mt.learnedDependsOn && mt.learnedDependsOn.length > 0) {
|
|
@@ -101647,8 +102618,6 @@ class LearnedTemplateProductBuilder {
|
|
|
101647
102618
|
normalizeLearnedTaskPercentages(taskDefinitions);
|
|
101648
102619
|
const workItemTypes = [...new Set(analyses.map((a3) => a3.story.type))];
|
|
101649
102620
|
const allTags = [...new Set(analyses.flatMap((a3) => a3.story.tags ?? []))];
|
|
101650
|
-
const estimations = analyses.map((a3) => a3.story.estimation ?? 0).filter((estimation) => estimation > 0);
|
|
101651
|
-
const avgEstimation = estimations.length > 0 ? Math.round(estimations.reduce((sum, estimation) => sum + estimation, 0) / estimations.length) : 0;
|
|
101652
102621
|
const storyIds = analyses.map((a3) => a3.story.id).join(", ");
|
|
101653
102622
|
return {
|
|
101654
102623
|
version: "1.0",
|
|
@@ -101662,12 +102631,7 @@ class LearnedTemplateProductBuilder {
|
|
|
101662
102631
|
tags: allTags.length > 0 ? { include: allTags } : undefined
|
|
101663
102632
|
},
|
|
101664
102633
|
tasks: taskDefinitions,
|
|
101665
|
-
estimation:
|
|
101666
|
-
strategy: "percentage",
|
|
101667
|
-
rounding: "none",
|
|
101668
|
-
minimumTaskPoints: 0,
|
|
101669
|
-
defaultParentEstimation: avgEstimation
|
|
101670
|
-
}
|
|
102634
|
+
estimation: learnedEstimationConfig(analyses.map((a3) => estimationSample(a3.story, a3.tasks)), estimationSource)
|
|
101671
102635
|
};
|
|
101672
102636
|
}
|
|
101673
102637
|
buildEmptyTemplate(story) {
|
|
@@ -101688,11 +102652,10 @@ class LearnedTemplateProductBuilder {
|
|
|
101688
102652
|
}
|
|
101689
102653
|
};
|
|
101690
102654
|
}
|
|
101691
|
-
buildSingleStoryTemplate(story, tasks) {
|
|
101692
|
-
const
|
|
102655
|
+
buildSingleStoryTemplate(story, tasks, estimationSource) {
|
|
102656
|
+
const shares = taskEstimateShares(tasks.map((task) => numericStoryEstimate(task.estimation)));
|
|
101693
102657
|
const taskDefinitions = tasks.map((task, index) => {
|
|
101694
|
-
const
|
|
101695
|
-
const estimationPercent = storyEstimation > 0 ? Math.round(taskEstimation / storyEstimation * 100) : 0;
|
|
102658
|
+
const estimationPercent = shares[index] ?? 0;
|
|
101696
102659
|
return {
|
|
101697
102660
|
id: this.generateTaskId(task.title, index),
|
|
101698
102661
|
title: this.extractTitlePattern(task.title, story.title),
|
|
@@ -101716,16 +102679,12 @@ class LearnedTemplateProductBuilder {
|
|
|
101716
102679
|
tags: story.tags?.length ? { include: story.tags } : undefined
|
|
101717
102680
|
},
|
|
101718
102681
|
tasks: taskDefinitions,
|
|
101719
|
-
estimation:
|
|
101720
|
-
strategy: "percentage",
|
|
101721
|
-
rounding: "none",
|
|
101722
|
-
minimumTaskPoints: 0,
|
|
101723
|
-
defaultParentEstimation: storyEstimation
|
|
101724
|
-
}
|
|
102682
|
+
estimation: learnedEstimationConfig([estimationSample(story, tasks)], estimationSource)
|
|
101725
102683
|
};
|
|
101726
102684
|
}
|
|
101727
102685
|
generateSuggestions(analyses, patterns, confidence, outliers) {
|
|
101728
102686
|
return [
|
|
102687
|
+
...this.suggestEstimationChecks(analyses),
|
|
101729
102688
|
...this.suggestConfidenceImprovements(analyses, confidence),
|
|
101730
102689
|
...this.suggestOutlierRemovals(outliers),
|
|
101731
102690
|
...this.suggestNamingImprovements(patterns),
|
|
@@ -101734,7 +102693,18 @@ class LearnedTemplateProductBuilder {
|
|
|
101734
102693
|
...this.suggestFilterImprovements(patterns)
|
|
101735
102694
|
];
|
|
101736
102695
|
}
|
|
101737
|
-
|
|
102696
|
+
suggestEstimationChecks(analyses) {
|
|
102697
|
+
const learned = inferEstimation(analyses.map((a3) => estimationSample(a3.story, a3.tasks)));
|
|
102698
|
+
const table = learned.conversion?.table;
|
|
102699
|
+
if (!table)
|
|
102700
|
+
return [];
|
|
102701
|
+
return learned.lowConfidenceValues.map((value) => ({
|
|
102702
|
+
type: "adjust-estimation",
|
|
102703
|
+
message: `The conversion for "${value}" (${table[value]}) was learned from a single Story; check it before relying on it.`,
|
|
102704
|
+
severity: "info"
|
|
102705
|
+
}));
|
|
102706
|
+
}
|
|
102707
|
+
generateVariations(analyses, patterns, mergedTasks, estimationSource) {
|
|
101738
102708
|
const confidenceScorer = new ConfidenceScorer;
|
|
101739
102709
|
const variations = [];
|
|
101740
102710
|
const coreTasks = mergedTasks.filter((mt) => {
|
|
@@ -101742,7 +102712,7 @@ class LearnedTemplateProductBuilder {
|
|
|
101742
102712
|
return storyCount / analyses.length >= 0.6;
|
|
101743
102713
|
});
|
|
101744
102714
|
if (coreTasks.length > 0 && coreTasks.length !== mergedTasks.length) {
|
|
101745
|
-
const coreTemplate = this.buildMergedTemplate(analyses, coreTasks);
|
|
102715
|
+
const coreTemplate = this.buildMergedTemplate(analyses, coreTasks, estimationSource);
|
|
101746
102716
|
coreTemplate.name = "Core Tasks Template";
|
|
101747
102717
|
coreTemplate.description = "Tasks appearing in 60%+ of analyzed stories";
|
|
101748
102718
|
variations.push({
|
|
@@ -101753,7 +102723,7 @@ class LearnedTemplateProductBuilder {
|
|
|
101753
102723
|
});
|
|
101754
102724
|
}
|
|
101755
102725
|
if (mergedTasks.length > 0) {
|
|
101756
|
-
const fullTemplate = this.buildMergedTemplate(analyses, mergedTasks);
|
|
102726
|
+
const fullTemplate = this.buildMergedTemplate(analyses, mergedTasks, estimationSource);
|
|
101757
102727
|
fullTemplate.name = "Comprehensive Template";
|
|
101758
102728
|
fullTemplate.description = "All tasks found across analyzed stories";
|
|
101759
102729
|
variations.push({
|
|
@@ -101876,6 +102846,23 @@ class LearnedTemplateProductBuilder {
|
|
|
101876
102846
|
return suggestions;
|
|
101877
102847
|
}
|
|
101878
102848
|
}
|
|
102849
|
+
function estimationSample(story, tasks) {
|
|
102850
|
+
return {
|
|
102851
|
+
storyEstimate: story.estimation,
|
|
102852
|
+
taskEstimates: tasks.map((task) => numericStoryEstimate(task.estimation))
|
|
102853
|
+
};
|
|
102854
|
+
}
|
|
102855
|
+
function learnedEstimationConfig(samples, source) {
|
|
102856
|
+
const learned = inferEstimation(samples);
|
|
102857
|
+
return {
|
|
102858
|
+
strategy: "percentage",
|
|
102859
|
+
rounding: "none",
|
|
102860
|
+
minimumTaskEstimate: 0,
|
|
102861
|
+
...source ? { source } : {},
|
|
102862
|
+
...learned.conversion ? { conversion: learned.conversion } : {},
|
|
102863
|
+
...learned.defaultStoryEstimate !== undefined ? { defaultParentEstimation: learned.defaultStoryEstimate } : {}
|
|
102864
|
+
};
|
|
102865
|
+
}
|
|
101879
102866
|
|
|
101880
102867
|
// ../atomize-core/src/services/template/task-merger.ts
|
|
101881
102868
|
class TaskMerger {
|
|
@@ -101969,15 +102956,15 @@ class TaskMerger {
|
|
|
101969
102956
|
for (const value of values) {
|
|
101970
102957
|
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
101971
102958
|
}
|
|
101972
|
-
let
|
|
102959
|
+
let mostCommon2 = values[0] ?? 0;
|
|
101973
102960
|
let maxCount = 0;
|
|
101974
102961
|
for (const [value, count] of counts) {
|
|
101975
|
-
if (count > maxCount || count === maxCount && value >
|
|
101976
|
-
|
|
102962
|
+
if (count > maxCount || count === maxCount && value > mostCommon2) {
|
|
102963
|
+
mostCommon2 = value;
|
|
101977
102964
|
maxCount = count;
|
|
101978
102965
|
}
|
|
101979
102966
|
}
|
|
101980
|
-
return
|
|
102967
|
+
return mostCommon2;
|
|
101981
102968
|
}
|
|
101982
102969
|
averagePriority(values) {
|
|
101983
102970
|
if (values.length === 0)
|
|
@@ -102082,27 +103069,28 @@ class StoryLearner {
|
|
|
102082
103069
|
this.learningSession = learningSession;
|
|
102083
103070
|
this.productBuilder = productBuilder;
|
|
102084
103071
|
}
|
|
102085
|
-
async learnFromStory(storyId) {
|
|
103072
|
+
async learnFromStory(storyId, options = {}) {
|
|
102086
103073
|
logger.info(`Learning template from story: ${storyId}`);
|
|
102087
|
-
const
|
|
102088
|
-
if (!
|
|
103074
|
+
const fetched = await this.platform.getWorkItem(storyId);
|
|
103075
|
+
if (!fetched) {
|
|
102089
103076
|
throw new TemplateGenerationError(`Story ${storyId} not found`);
|
|
102090
103077
|
}
|
|
103078
|
+
const story = withEstimationSource(fetched, options.estimationSource);
|
|
102091
103079
|
const tasks = await this.platform.getChildren(storyId);
|
|
102092
103080
|
if (!tasks || tasks.length === 0) {
|
|
102093
103081
|
throw new TemplateGenerationError(`Story ${storyId} has no child tasks to learn from`);
|
|
102094
103082
|
}
|
|
102095
103083
|
logger.info(`Found ${tasks.length} tasks to analyze`);
|
|
102096
|
-
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks);
|
|
103084
|
+
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks, options.estimationSource);
|
|
102097
103085
|
logger.info("Template learned successfully");
|
|
102098
103086
|
return template;
|
|
102099
103087
|
}
|
|
102100
|
-
async learnFromStories(storyIds) {
|
|
103088
|
+
async learnFromStories(storyIds, options = {}) {
|
|
102101
103089
|
logger.info(`Learning template from ${storyIds.length} stories: ${storyIds.join(", ")}`);
|
|
102102
103090
|
if (storyIds.length === 0) {
|
|
102103
103091
|
throw new TemplateGenerationError("No story IDs provided");
|
|
102104
103092
|
}
|
|
102105
|
-
const results = await Promise.allSettled(storyIds.map((id) => this.analyzeStory(id)));
|
|
103093
|
+
const results = await Promise.allSettled(storyIds.map((id) => this.analyzeStory(id, options.estimationSource)));
|
|
102106
103094
|
const analyses = [];
|
|
102107
103095
|
const skipped = [];
|
|
102108
103096
|
for (let i3 = 0;i3 < results.length; i3++) {
|
|
@@ -102134,9 +103122,9 @@ class StoryLearner {
|
|
|
102134
103122
|
confidence,
|
|
102135
103123
|
outliers
|
|
102136
103124
|
} = this.learningSession.run(analyses);
|
|
102137
|
-
const template = this.productBuilder.buildMergedTemplate(analyses, mergedTasks);
|
|
103125
|
+
const template = this.productBuilder.buildMergedTemplate(analyses, mergedTasks, options.estimationSource);
|
|
102138
103126
|
const suggestions = this.productBuilder.generateSuggestions(analyses, patterns, confidence, outliers);
|
|
102139
|
-
const variations = this.productBuilder.generateVariations(analyses, patterns, mergedTasks);
|
|
103127
|
+
const variations = this.productBuilder.generateVariations(analyses, patterns, mergedTasks, options.estimationSource);
|
|
102140
103128
|
logger.info(`Multi-story learning complete: ${analyses.length} analyzed, ${skipped.length} skipped, confidence: ${confidence.level} (${confidence.overall}%)`);
|
|
102141
103129
|
return {
|
|
102142
103130
|
analyses,
|
|
@@ -102149,11 +103137,12 @@ class StoryLearner {
|
|
|
102149
103137
|
outliers
|
|
102150
103138
|
};
|
|
102151
103139
|
}
|
|
102152
|
-
async analyzeStory(storyId) {
|
|
102153
|
-
const
|
|
102154
|
-
if (!
|
|
103140
|
+
async analyzeStory(storyId, estimationSource) {
|
|
103141
|
+
const fetched = await this.platform.getWorkItem(storyId);
|
|
103142
|
+
if (!fetched) {
|
|
102155
103143
|
throw new TemplateGenerationError(`Story ${storyId} not found`);
|
|
102156
103144
|
}
|
|
103145
|
+
const story = withEstimationSource(fetched, estimationSource);
|
|
102157
103146
|
const tasks = await this.platform.getChildren(storyId);
|
|
102158
103147
|
const warnings = [];
|
|
102159
103148
|
if (tasks.length === 0) {
|
|
@@ -102168,7 +103157,7 @@ class StoryLearner {
|
|
|
102168
103157
|
if (tasks.length === 1) {
|
|
102169
103158
|
warnings.push(`Story ${storyId} has only 1 task, which may not be representative`);
|
|
102170
103159
|
}
|
|
102171
|
-
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks);
|
|
103160
|
+
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks, estimationSource);
|
|
102172
103161
|
return { story, tasks, template, warnings };
|
|
102173
103162
|
}
|
|
102174
103163
|
extractTitlePattern(taskTitle, storyTitle) {
|
|
@@ -102181,6 +103170,11 @@ class StoryLearner {
|
|
|
102181
103170
|
return this.productBuilder.detectActivity(title, description);
|
|
102182
103171
|
}
|
|
102183
103172
|
}
|
|
103173
|
+
function withEstimationSource(story, source) {
|
|
103174
|
+
if (!source)
|
|
103175
|
+
return story;
|
|
103176
|
+
return { ...story, estimation: readStoryEstimate(story, { kind: "override", field: source }) };
|
|
103177
|
+
}
|
|
102184
103178
|
|
|
102185
103179
|
// src/cli/commands/template/template-create.command.ts
|
|
102186
103180
|
init_template_library();
|
|
@@ -102576,6 +103570,8 @@ mixins: string[] # optional — list of mixin references or file paths (e
|
|
|
102576
103570
|
# Mixins contribute tasks only. Child tasks override mixin tasks with the same id.
|
|
102577
103571
|
# Template names are NOT valid mixin sources.
|
|
102578
103572
|
|
|
103573
|
+
taskType: string # optional — work item type Tasks are created as (e.g. "Task", "Sub-task"). Omit to use the platform default.
|
|
103574
|
+
|
|
102579
103575
|
filter: # required (unless inherited via extends)
|
|
102580
103576
|
workItemTypes: string[] # e.g. ["User Story"], ["Bug"]
|
|
102581
103577
|
states: string[] # e.g. ["New", "Active", "Approved"] — items must be in one of these states
|
|
@@ -102606,6 +103602,7 @@ tasks: # required, minimum 1 item
|
|
|
102606
103602
|
tags: string[] # optional
|
|
102607
103603
|
id: string # optional, required if this task is referenced in dependsOn
|
|
102608
103604
|
dependsOn: string[] # optional, references other task ids
|
|
103605
|
+
repeat: number # optional, default 1 — create this many identical Tasks from this one definition (see rule 9)
|
|
102609
103606
|
condition: # optional — skip this task entirely when condition is false
|
|
102610
103607
|
# Standard fields (case-sensitive, always lowercase camelCase — NOT the ADO REST field
|
|
102611
103608
|
# names like "System.State" or "Microsoft.VSTS.Common.Priority"):
|
|
@@ -102627,11 +103624,21 @@ tasks: # required, minimum 1 item
|
|
|
102627
103624
|
estimation: # optional
|
|
102628
103625
|
strategy: "percentage"
|
|
102629
103626
|
rounding: "nearest" | "up" | "down" | "none" # default "none" — omit unless the user asks for rounding
|
|
102630
|
-
|
|
103627
|
+
normalize: "auto" | "always" | "never" # optional, default "auto" — set "never" only if the user wants percentages used exactly as written
|
|
103628
|
+
minimumTaskEstimate: number # optional, default 0 — minimum per task, in the Task's unit (hours on Azure DevOps)
|
|
103629
|
+
source: string # optional — Story field reference name holding the Story Estimate (e.g. "Custom.TShirtSize"). Omit to use the platform default (story points / effort / size).
|
|
103630
|
+
conversion: # optional — Story Estimate → Task-unit total before the split. Set exactly ONE of factor or table.
|
|
103631
|
+
factor: number # e.g. 4 when 1 point = 4 hours
|
|
103632
|
+
table: # exact value → amount, e.g. { S: 2, M: 4, L: 5, XL: 13 }
|
|
103633
|
+
multipliers: boolean # optional, only with a table of size keys: "0.3XL" = 0.3 × XL
|
|
103634
|
+
targetFields: string[] # optional — Task fields that receive the estimate, replacing the platform defaults. Rarely needed.
|
|
103635
|
+
ifParentHasNoEstimation: "warn" | "skip" | "use-default" # optional, default "warn" (tasks get blank estimates)
|
|
103636
|
+
defaultParentEstimation: number | string # optional — Story Estimate to assume with use-default, e.g. 5 or "M"
|
|
102631
103637
|
|
|
102632
103638
|
validation: # optional
|
|
102633
|
-
minTasks: number
|
|
103639
|
+
minTasks: number # each copy of a repeated task counts
|
|
102634
103640
|
maxTasks: number
|
|
103641
|
+
maxRepeat: number # optional, default 20 — the highest repeat any task may use
|
|
102635
103642
|
totalEstimationMustBe: number # usually 100
|
|
102636
103643
|
totalEstimationRange:
|
|
102637
103644
|
min: number
|
|
@@ -102646,7 +103653,7 @@ metadata: # optional
|
|
|
102646
103653
|
|
|
102647
103654
|
CONSTRAINTS (you must follow these):
|
|
102648
103655
|
1. Estimation sums:
|
|
102649
|
-
- Simple templates (no task-level conditions): estimationPercent values MUST sum to exactly 100.
|
|
103656
|
+
- Simple templates (no task-level conditions): estimationPercent values MUST sum to exactly 100. A repeated task contributes its estimationPercent once per copy.
|
|
102650
103657
|
- Multi-archetype templates (tasks use conditions to target different story types): each archetype's active task set should sum to ~100% independently. The total across all declared tasks will naturally exceed 100 — this is expected and valid.
|
|
102651
103658
|
2. If a task uses dependsOn, all referenced IDs must exist on other tasks in the same template.
|
|
102652
103659
|
3. workItemTypes values are case-sensitive strings matching ADO work item types.
|
|
@@ -102672,6 +103679,17 @@ CONSTRAINTS (you must follow these):
|
|
|
102672
103679
|
estimationPercentCondition:
|
|
102673
103680
|
- condition: { field: priority, operator: lte, value: 2 }
|
|
102674
103681
|
percent: 35 # override for critical bugs (priority 1 or 2)
|
|
103682
|
+
8. ESTIMATION FIELDS: only set taskType, estimation.source, estimation.conversion or estimation.targetFields when the user describes non-default sizing (for example t-shirt sizes, a custom size field, or "1 point = 4 hours"), or when the workspace context lists an estimation field that matches the description.
|
|
103683
|
+
- Use field reference names and work item types exactly as given in the workspace context; never invent them.
|
|
103684
|
+
- With a conversion table on a picklist source, use the picklist's allowed values exactly as keys and cover every one.
|
|
103685
|
+
- For a numeric scale where hours grow faster than points, use a table with numeric keys instead of a factor.
|
|
103686
|
+
- Conditions on estimation compare the raw Story Estimate: with size categories use equals/not-equals (e.g. value: "L"), never gt/lt/gte/lte.
|
|
103687
|
+
9. REPEATED TASKS — when the user wants several identical Tasks from the same definition (e.g. "one review task per developer, 4 developers"), write ONE task with repeat: N instead of copying the task block N times.
|
|
103688
|
+
- Atomize titles the copies "<title> (1)", "<title> (2)", ... so do not number the title yourself.
|
|
103689
|
+
- Each copy gets the task's full estimationPercent, so every copy counts toward the total: repeat: 4 at 10% contributes 40%.
|
|
103690
|
+
- NEVER combine repeat with dependsOn, and never list a repeated task's id in another task's dependsOn.
|
|
103691
|
+
- repeat must be a whole number, at most 20 unless you also set validation.maxRepeat to at least that number. Only raise maxRepeat when the user explicitly asks for more than 20 copies.
|
|
103692
|
+
- Every copy is identical; when copies need different assignees, estimates or fields, write separate tasks instead.
|
|
102675
103693
|
|
|
102676
103694
|
EXAMPLES:
|
|
102677
103695
|
|
|
@@ -102875,7 +103893,32 @@ filter:
|
|
|
102875
103893
|
tasks:
|
|
102876
103894
|
- id: "implement"
|
|
102877
103895
|
title: "Implement (Mobile): \${story.title}"
|
|
102878
|
-
estimationPercent: 60
|
|
103896
|
+
estimationPercent: 60
|
|
103897
|
+
|
|
103898
|
+
Example 7 — Repeated identical tasks (one review per developer):
|
|
103899
|
+
version: "1.0"
|
|
103900
|
+
name: "Team Review"
|
|
103901
|
+
description: "Implementation plus one review conclusions task for each of the four developers."
|
|
103902
|
+
filter:
|
|
103903
|
+
workItemTypes: ["User Story"]
|
|
103904
|
+
states: ["Active"]
|
|
103905
|
+
excludeIfHasTasks: true
|
|
103906
|
+
tasks:
|
|
103907
|
+
- title: "Implementation"
|
|
103908
|
+
estimationPercent: 60
|
|
103909
|
+
activity: "Development"
|
|
103910
|
+
assignTo: "@ParentAssignee"
|
|
103911
|
+
- title: "Add review conclusions"
|
|
103912
|
+
estimationPercent: 10
|
|
103913
|
+
repeat: 4
|
|
103914
|
+
activity: "Development"
|
|
103915
|
+
assignTo: "@Unassigned"
|
|
103916
|
+
estimation:
|
|
103917
|
+
strategy: "percentage"
|
|
103918
|
+
metadata:
|
|
103919
|
+
category: "Agile"
|
|
103920
|
+
difficulty: "beginner"
|
|
103921
|
+
estimationGuidelines: "60% + 4 copies × 10% = 100%."`;
|
|
102879
103922
|
function buildSystemPrompt() {
|
|
102880
103923
|
return SYSTEM_PROMPT;
|
|
102881
103924
|
}
|
|
@@ -102890,7 +103933,7 @@ ${description}`;
|
|
|
102890
103933
|
Observed patterns from this user's Azure DevOps workspace:
|
|
102891
103934
|
${groundingContext}
|
|
102892
103935
|
|
|
102893
|
-
Use these patterns as concrete examples when choosing task names, estimation percentages, and conditions. Adapt them to fit the description above — do not copy verbatim.
|
|
103936
|
+
Use these patterns as concrete examples when choosing task names, estimation percentages, and conditions. Adapt them to fit the description above — do not copy verbatim. If the context has an "estimation" section, it lists the platform's default estimation fields and the Story fields that can hold a Story Estimate, with their allowed values; follow rule 8 when using them.
|
|
102894
103937
|
---`;
|
|
102895
103938
|
}
|
|
102896
103939
|
if (prevErrors && prevErrors.length > 0) {
|
|
@@ -102944,6 +103987,7 @@ init_prompt_utilities();
|
|
|
102944
103987
|
|
|
102945
103988
|
// src/cli/commands/template/template-wizard.ts
|
|
102946
103989
|
init_dist6();
|
|
103990
|
+
init_schema2();
|
|
102947
103991
|
init_errors();
|
|
102948
103992
|
init_estimation_normalizer();
|
|
102949
103993
|
init_source();
|
|
@@ -103189,7 +104233,7 @@ var STORY_FIELDS = [
|
|
|
103189
104233
|
{ label: "Tags (array)", value: "tags" },
|
|
103190
104234
|
{ label: "Title", value: "title" },
|
|
103191
104235
|
{ label: "State", value: "state" },
|
|
103192
|
-
{ label: "
|
|
104236
|
+
{ label: "Story Estimate (raw value, e.g. points or size)", value: "estimation" },
|
|
103193
104237
|
{ label: "Priority", value: "priority" },
|
|
103194
104238
|
{ label: "Description", value: "description" },
|
|
103195
104239
|
{ label: "Assigned To", value: "assignedTo" },
|
|
@@ -103438,12 +104482,22 @@ async function configureTaskTags(defaults) {
|
|
|
103438
104482
|
return feature.data.length > 0 ? feature.data : undefined;
|
|
103439
104483
|
}
|
|
103440
104484
|
async function configureAdvancedTaskOptions(storyFieldSchemas, defaults) {
|
|
103441
|
-
const
|
|
103442
|
-
message: "
|
|
103443
|
-
placeholder: "
|
|
103444
|
-
initialValue: defaults?.
|
|
104485
|
+
const repeatRaw = assertNotCancelled(await text({
|
|
104486
|
+
message: "How many identical Tasks should this create?",
|
|
104487
|
+
placeholder: "1",
|
|
104488
|
+
initialValue: String(defaults?.repeat ?? 1),
|
|
104489
|
+
validate: Validators.positiveInteger("Number of Tasks")
|
|
103445
104490
|
}));
|
|
103446
|
-
const
|
|
104491
|
+
const repeat = Number(repeatRaw);
|
|
104492
|
+
let dependsOn = [];
|
|
104493
|
+
if (repeat === 1) {
|
|
104494
|
+
const dependsOnRaw = assertNotCancelled(await text({
|
|
104495
|
+
message: "Depends on task IDs (comma-separated, optional):",
|
|
104496
|
+
placeholder: "e.g. task-setup, task-db, task-build, task-test",
|
|
104497
|
+
initialValue: defaults?.dependsOn?.join(", ") ?? ""
|
|
104498
|
+
}));
|
|
104499
|
+
dependsOn = Filters.commaSeparated(dependsOnRaw);
|
|
104500
|
+
}
|
|
103447
104501
|
const addCondition = assertNotCancelled(await confirm({ message: "Add a task condition?", initialValue: false }));
|
|
103448
104502
|
let condition;
|
|
103449
104503
|
if (addCondition) {
|
|
@@ -103456,6 +104510,8 @@ async function configureAdvancedTaskOptions(storyFieldSchemas, defaults) {
|
|
|
103456
104510
|
validate: Validators.priorityRange
|
|
103457
104511
|
}));
|
|
103458
104512
|
const result = {};
|
|
104513
|
+
if (repeat > 1)
|
|
104514
|
+
result.repeat = repeat;
|
|
103459
104515
|
if (dependsOn.length > 0)
|
|
103460
104516
|
result.dependsOn = dependsOn;
|
|
103461
104517
|
if (condition)
|
|
@@ -103499,6 +104555,8 @@ async function buildTaskDefinition(isFirstTask, includeAdvanced, fieldSchemas, s
|
|
|
103499
104555
|
}
|
|
103500
104556
|
const advanced = await configureAdvancedTaskOptions(storyFieldSchemas, defaults);
|
|
103501
104557
|
Object.assign(taskDef, advanced);
|
|
104558
|
+
} else if (defaults?.repeat !== undefined) {
|
|
104559
|
+
taskDef.repeat = defaults.repeat;
|
|
103502
104560
|
}
|
|
103503
104561
|
const customFields = await configureCustomFields(fieldSchemas, storyFieldSchemas);
|
|
103504
104562
|
if (Object.keys(customFields).length > 0) {
|
|
@@ -103527,6 +104585,7 @@ function normalizeEstimations(tasks) {
|
|
|
103527
104585
|
}
|
|
103528
104586
|
}
|
|
103529
104587
|
async function previewTemplate(template, ctx) {
|
|
104588
|
+
await offerRepeatLimit(template);
|
|
103530
104589
|
displayTemplatePreview(template);
|
|
103531
104590
|
const action = assertNotCancelled(await select({
|
|
103532
104591
|
message: "What would you like to do?",
|
|
@@ -103539,6 +104598,21 @@ async function previewTemplate(template, ctx) {
|
|
|
103539
104598
|
}));
|
|
103540
104599
|
return await handlePreviewAction(action, template, ctx);
|
|
103541
104600
|
}
|
|
104601
|
+
async function offerRepeatLimit(template) {
|
|
104602
|
+
const highest = Math.max(...template.tasks.map(copyCount));
|
|
104603
|
+
const limit = template.validation?.maxRepeat ?? DEFAULT_MAX_REPEAT;
|
|
104604
|
+
if (highest <= limit)
|
|
104605
|
+
return;
|
|
104606
|
+
const raise = assertNotCancelled(await confirm({
|
|
104607
|
+
message: `A task creates ${highest} copies, above the limit of ${limit}. Raise the limit (validation.maxRepeat) to ${highest}?`,
|
|
104608
|
+
initialValue: true
|
|
104609
|
+
}));
|
|
104610
|
+
if (raise)
|
|
104611
|
+
template.validation = { ...template.validation, maxRepeat: highest };
|
|
104612
|
+
}
|
|
104613
|
+
function totalTaskPercent(tasks) {
|
|
104614
|
+
return tasks.reduce((sum2, task) => sum2 + (task.estimationPercent || 0) * copyCount(task), 0);
|
|
104615
|
+
}
|
|
103542
104616
|
function displayTemplatePreview(template) {
|
|
103543
104617
|
output4.print(source_default.cyan(`
|
|
103544
104618
|
Template Preview
|
|
@@ -103579,13 +104653,16 @@ Filter Configuration:`));
|
|
|
103579
104653
|
}
|
|
103580
104654
|
}
|
|
103581
104655
|
function displayTasksSummary(template) {
|
|
104656
|
+
const generated = template.tasks.reduce((sum2, task) => sum2 + copyCount(task), 0);
|
|
104657
|
+
const counted = generated === template.tasks.length ? "" : `, ${generated} generated`;
|
|
103582
104658
|
output4.print(source_default.bold(`
|
|
103583
|
-
Tasks (${template.tasks.length}):`));
|
|
103584
|
-
const totalEstimation = template.tasks
|
|
104659
|
+
Tasks (${template.tasks.length}${counted}):`));
|
|
104660
|
+
const totalEstimation = totalTaskPercent(template.tasks);
|
|
103585
104661
|
template.tasks.forEach((task, index) => {
|
|
103586
104662
|
const percent = task.estimationPercent || 0;
|
|
103587
104663
|
const bar = "■".repeat(Math.round(percent / 5));
|
|
103588
|
-
|
|
104664
|
+
const copies = copyCount(task) > 1 ? source_default.cyan(` ×${copyCount(task)}`) : "";
|
|
104665
|
+
output4.print(` ${index + 1}. ${task.title}${copies} ${source_default.gray(`[${percent}%]`)} ${source_default.green(bar)}`);
|
|
103589
104666
|
if (task.description) {
|
|
103590
104667
|
output4.print(source_default.gray(` ${task.description}`));
|
|
103591
104668
|
}
|
|
@@ -103615,6 +104692,9 @@ Validation Rules:`));
|
|
|
103615
104692
|
if (template.validation.minTasks || template.validation.maxTasks) {
|
|
103616
104693
|
output4.print(` Task count: ${template.validation.minTasks || "no min"} - ${template.validation.maxTasks || "no max"}`);
|
|
103617
104694
|
}
|
|
104695
|
+
if (template.validation.maxRepeat !== undefined) {
|
|
104696
|
+
output4.print(` Copies per task: up to ${template.validation.maxRepeat}`);
|
|
104697
|
+
}
|
|
103618
104698
|
}
|
|
103619
104699
|
function displayMetadata(template) {
|
|
103620
104700
|
if (!template.metadata)
|
|
@@ -103679,8 +104759,9 @@ Editing Tasks
|
|
|
103679
104759
|
Editing Estimation Settings
|
|
103680
104760
|
`));
|
|
103681
104761
|
const { configureEstimation: configureEstimation2 } = await Promise.resolve().then(() => (init_template_wizard_helper_command(), exports_template_wizard_helper_command));
|
|
103682
|
-
const estimation = await configureEstimation2(template.estimation);
|
|
104762
|
+
const { estimation, taskType } = await configureEstimation2(template.estimation, ctx.estimationCtx ?? { storyFields: ctx.storyFieldSchemas, workItemTypes: ctx.filterCtx.workItemTypes }, template.taskType);
|
|
103683
104763
|
template.estimation = estimation;
|
|
104764
|
+
template.taskType = taskType;
|
|
103684
104765
|
}).with("validation", async () => {
|
|
103685
104766
|
output4.print(source_default.cyan(`
|
|
103686
104767
|
Editing Validation Rules
|
|
@@ -103745,7 +104826,7 @@ function showStepHint(stepName) {
|
|
|
103745
104826
|
const hints = {
|
|
103746
104827
|
filter: "Tip: Use filters to select which work items this template applies to",
|
|
103747
104828
|
tasks: "Tip: Break work into clear, actionable tasks. Estimation percentages will be normalized to 100%. If you plan on set dependencies, please set task IDs.",
|
|
103748
|
-
estimation: "Tip: Choose how
|
|
104829
|
+
estimation: "Tip: Choose how Story Estimates turn into Task hours and where they are written",
|
|
103749
104830
|
validation: "Tip: Add constraints to ensure templates are used correctly",
|
|
103750
104831
|
metadata: "Tip: Metadata helps others understand when to use this template"
|
|
103751
104832
|
};
|
|
@@ -103815,13 +104896,17 @@ async function shouldAddMoreTasks(taskCounter) {
|
|
|
103815
104896
|
}));
|
|
103816
104897
|
}
|
|
103817
104898
|
async function handleEstimationNormalization(tasks) {
|
|
103818
|
-
const totalEstimation = tasks
|
|
104899
|
+
const totalEstimation = totalTaskPercent(tasks);
|
|
103819
104900
|
if (totalEstimation === 100) {
|
|
103820
104901
|
output4.print(source_default.green("✓ Total estimation is 100%"));
|
|
103821
104902
|
return;
|
|
103822
104903
|
}
|
|
103823
104904
|
output4.print(source_default.yellow(`
|
|
103824
104905
|
Warning: Total estimation is ${totalEstimation}% (should be 100%)`));
|
|
104906
|
+
if (tasks.some((task) => copyCount(task) > 1)) {
|
|
104907
|
+
output4.print(source_default.yellow(" Each copy of a repeated task counts toward the total; adjust the percentages to reach 100%."));
|
|
104908
|
+
return;
|
|
104909
|
+
}
|
|
103825
104910
|
const shouldNormalize = await promptForNormalization();
|
|
103826
104911
|
if (shouldNormalize) {
|
|
103827
104912
|
normalizeEstimations(tasks);
|
|
@@ -103859,7 +104944,7 @@ async function editTasksInteractively(tasks, fieldSchemas, storyFieldSchemas) {
|
|
|
103859
104944
|
Tasks (${currentTasks.length}) — total: ${totalLabel}
|
|
103860
104945
|
`));
|
|
103861
104946
|
currentTasks.forEach((t3, i3) => {
|
|
103862
|
-
output4.print(source_default.gray(` ${i3 + 1}. ${t3.title} [${t3.estimationPercent ?? 0}%]`));
|
|
104947
|
+
output4.print(source_default.gray(` ${i3 + 1}. ${t3.title}${copyCount(t3) > 1 ? ` ×${copyCount(t3)}` : ""} [${t3.estimationPercent ?? 0}%]`));
|
|
103863
104948
|
});
|
|
103864
104949
|
output4.blankLine();
|
|
103865
104950
|
};
|
|
@@ -103881,7 +104966,7 @@ Tasks (${currentTasks.length}) — total: ${totalLabel}
|
|
|
103881
104966
|
const idx = assertNotCancelled(await select({
|
|
103882
104967
|
message: "Select task to edit:",
|
|
103883
104968
|
options: currentTasks.map((t3, i3) => ({
|
|
103884
|
-
label: `${i3 + 1}. ${t3.title} [${t3.estimationPercent ?? 0}%]`,
|
|
104969
|
+
label: `${i3 + 1}. ${t3.title}${copyCount(t3) > 1 ? ` ×${copyCount(t3)}` : ""} [${t3.estimationPercent ?? 0}%]`,
|
|
103885
104970
|
value: i3
|
|
103886
104971
|
}))
|
|
103887
104972
|
}));
|
|
@@ -103898,7 +104983,7 @@ Tasks (${currentTasks.length}) — total: ${totalLabel}
|
|
|
103898
104983
|
const idx = assertNotCancelled(await select({
|
|
103899
104984
|
message: "Select task to remove:",
|
|
103900
104985
|
options: currentTasks.map((t3, i3) => ({
|
|
103901
|
-
label: `${i3 + 1}. ${t3.title} [${t3.estimationPercent ?? 0}%]`,
|
|
104986
|
+
label: `${i3 + 1}. ${t3.title}${copyCount(t3) > 1 ? ` ×${copyCount(t3)}` : ""} [${t3.estimationPercent ?? 0}%]`,
|
|
103902
104987
|
value: i3
|
|
103903
104988
|
}))
|
|
103904
104989
|
}));
|
|
@@ -103943,6 +105028,7 @@ Customize Template
|
|
|
103943
105028
|
const adapter = await createAzureDevOpsAdapter(profile);
|
|
103944
105029
|
const metadataReader = requireProjectMetadataReader(adapter);
|
|
103945
105030
|
const savedQueryReader = requireSavedQueryReader(adapter);
|
|
105031
|
+
const estimationDefaults2 = adapter.getEstimationDefaults();
|
|
103946
105032
|
const [
|
|
103947
105033
|
taskSchemas,
|
|
103948
105034
|
liveWorkItemTypes,
|
|
@@ -103951,7 +105037,7 @@ Customize Template
|
|
|
103951
105037
|
liveTeams,
|
|
103952
105038
|
liveSavedQueries
|
|
103953
105039
|
] = await Promise.all([
|
|
103954
|
-
metadataReader.getFieldSchemas(
|
|
105040
|
+
metadataReader.getFieldSchemas(template.taskType ?? estimationDefaults2.taskWorkItemType),
|
|
103955
105041
|
metadataReader.getWorkItemTypes(),
|
|
103956
105042
|
metadataReader.getAreaPaths(),
|
|
103957
105043
|
metadataReader.getIterationPaths(),
|
|
@@ -103960,6 +105046,7 @@ Customize Template
|
|
|
103960
105046
|
]);
|
|
103961
105047
|
return {
|
|
103962
105048
|
metadataReader,
|
|
105049
|
+
estimationDefaults: estimationDefaults2,
|
|
103963
105050
|
fieldSchemas: taskSchemas,
|
|
103964
105051
|
filterCtx: {
|
|
103965
105052
|
workItemTypes: liveWorkItemTypes,
|
|
@@ -104007,6 +105094,7 @@ Editing Name & Description
|
|
|
104007
105094
|
let filterCtx;
|
|
104008
105095
|
let fieldSchemas;
|
|
104009
105096
|
let adapterForWizard;
|
|
105097
|
+
let estimationDefaults;
|
|
104010
105098
|
try {
|
|
104011
105099
|
const conn = await connectionPromise;
|
|
104012
105100
|
if (!wasAlreadyConnected)
|
|
@@ -104014,6 +105102,7 @@ Editing Name & Description
|
|
|
104014
105102
|
filterCtx = conn.filterCtx;
|
|
104015
105103
|
fieldSchemas = conn.fieldSchemas;
|
|
104016
105104
|
adapterForWizard = conn.metadataReader;
|
|
105105
|
+
estimationDefaults = conn.estimationDefaults;
|
|
104017
105106
|
} catch (err) {
|
|
104018
105107
|
if (!wasAlreadyConnected)
|
|
104019
105108
|
connectSpinner.stop("Connection failed");
|
|
@@ -104025,6 +105114,12 @@ Editing Name & Description
|
|
|
104025
105114
|
}
|
|
104026
105115
|
let storyFieldSchemas = [];
|
|
104027
105116
|
let storySchemasFetched = false;
|
|
105117
|
+
const estimationContext = async () => ({
|
|
105118
|
+
storyFields: storyFieldSchemas.length ? storyFieldSchemas : await adapterForWizard.getFieldSchemas(),
|
|
105119
|
+
workItemTypes: filterCtx.workItemTypes,
|
|
105120
|
+
defaults: estimationDefaults,
|
|
105121
|
+
getTaskFields: (type) => adapterForWizard.getFieldSchemas(type)
|
|
105122
|
+
});
|
|
104028
105123
|
for (const section of ["filter", "tasks", "estimation", "validation", "metadata"]) {
|
|
104029
105124
|
if (!sections.includes(section))
|
|
104030
105125
|
continue;
|
|
@@ -104062,7 +105157,14 @@ Editing Tasks
|
|
|
104062
105157
|
output5.print(source_default.cyan(`
|
|
104063
105158
|
Editing Estimation Settings
|
|
104064
105159
|
`));
|
|
104065
|
-
|
|
105160
|
+
if (!storySchemasFetched) {
|
|
105161
|
+
const wit = template.filter.workItemTypes?.[0];
|
|
105162
|
+
storyFieldSchemas = wit ? await adapterForWizard.getFieldSchemas(wit) : [];
|
|
105163
|
+
storySchemasFetched = true;
|
|
105164
|
+
}
|
|
105165
|
+
const { estimation, taskType } = await configureEstimation(template.estimation, await estimationContext(), template.taskType);
|
|
105166
|
+
template.estimation = estimation;
|
|
105167
|
+
template.taskType = taskType;
|
|
104066
105168
|
break;
|
|
104067
105169
|
}
|
|
104068
105170
|
case "validation": {
|
|
@@ -104103,7 +105205,8 @@ Editing Metadata
|
|
|
104103
105205
|
filterCtx,
|
|
104104
105206
|
fieldSchemas,
|
|
104105
105207
|
storyFieldSchemas,
|
|
104106
|
-
workItemType: template.filter.workItemTypes?.[0]
|
|
105208
|
+
workItemType: template.filter.workItemTypes?.[0],
|
|
105209
|
+
estimationCtx: await estimationContext()
|
|
104107
105210
|
};
|
|
104108
105211
|
const confirmed = await previewTemplate(template, wizardCtx);
|
|
104109
105212
|
if (!confirmed) {
|
|
@@ -104421,7 +105524,7 @@ async function handlePreviewLoop(template, options) {
|
|
|
104421
105524
|
// src/cli/commands/template/template-create.command.ts
|
|
104422
105525
|
init_template_wizard_helper_command();
|
|
104423
105526
|
var defaultOutput = createCommandOutput(resolveCommandOutputPolicy({}));
|
|
104424
|
-
var templateCreateCommand = new Command("create").description("Create a new template interactively").option("--type <type>", "Create as template or mixin").option("--from <name>", "Start from an existing template").option("--from-stories <ids>", "Learn template from multiple stories (comma-separated IDs)").option("-p, --platform <platform>", "Platform to use", "azure-devops").option("--profile <name>", "Connect to ADO using a named profile for field suggestions (uses default profile if omitted)").option("--scratch", "Create from scratch (skip mode selection)").option("--ai", "Use AI-assisted generation (describe the template in natural language)").option("--ground", "Ground AI generation with patterns from your Azure DevOps workspace").option("--save-as <name>", "Name to save the template under").option("--open", "Open the saved Atomize YAML file in a supported editor", false).option("-q, --quiet", "Suppress non-essential output", false).action(async (options) => {
|
|
105527
|
+
var templateCreateCommand = new Command("create").description("Create a new template interactively").option("--type <type>", "Create as template or mixin").option("--from <name>", "Start from an existing template").option("--from-stories <ids>", "Learn template from multiple stories (comma-separated IDs)").option("--estimation-source <field>", "With --from-stories, read each Story Estimate from this field reference name (e.g. Custom.TShirtSize) and write it as estimation.source").option("-p, --platform <platform>", "Platform to use", "azure-devops").option("--profile <name>", "Connect to ADO using a named profile for field suggestions (uses default profile if omitted)").option("--scratch", "Create from scratch (skip mode selection)").option("--ai", "Use AI-assisted generation (describe the template in natural language)").option("--ground", "Ground AI generation with patterns from your Azure DevOps workspace").option("--save-as <name>", "Name to save the template under").option("--open", "Open the saved Atomize YAML file in a supported editor", false).option("-q, --quiet", "Suppress non-essential output", false).action(async (options) => {
|
|
104425
105528
|
const output9 = createCommandOutput(resolveCommandOutputPolicy({ quiet: options.quiet, verbose: false }));
|
|
104426
105529
|
try {
|
|
104427
105530
|
output9.intro(" Atomize — Template Creator");
|
|
@@ -104680,7 +105783,7 @@ async function createFromStories(options) {
|
|
|
104680
105783
|
const learner = new StoryLearner(learningPlatform);
|
|
104681
105784
|
const learnSpinner = createManagedSpinner();
|
|
104682
105785
|
learnSpinner.start(`Learning from ${storyIds.length} stories...`);
|
|
104683
|
-
const result = await learner.learnFromStories(storyIds);
|
|
105786
|
+
const result = await learner.learnFromStories(storyIds, { estimationSource: options.estimationSource });
|
|
104684
105787
|
learnSpinner.stop(`Analyzed ${result.analyses.length} stories ✓`);
|
|
104685
105788
|
displayMultiStoryResults(result);
|
|
104686
105789
|
if (result.variations.length > 1) {
|
|
@@ -104776,8 +105879,9 @@ Create Template from Scratch
|
|
|
104776
105879
|
const adapter = await createAzureDevOpsAdapter(profile);
|
|
104777
105880
|
const metadataReader = requireProjectMetadataReader(adapter);
|
|
104778
105881
|
const savedQueryReader = requireSavedQueryReader(adapter);
|
|
105882
|
+
const estimationDefaults2 = adapter.getEstimationDefaults();
|
|
104779
105883
|
const [taskSchemas, liveWorkItemTypes, liveAreaPaths, liveIterationPaths, liveTeams, liveSavedQueries] = await Promise.all([
|
|
104780
|
-
metadataReader.getFieldSchemas(
|
|
105884
|
+
metadataReader.getFieldSchemas(estimationDefaults2.taskWorkItemType),
|
|
104781
105885
|
metadataReader.getWorkItemTypes(),
|
|
104782
105886
|
metadataReader.getAreaPaths(),
|
|
104783
105887
|
metadataReader.getIterationPaths(),
|
|
@@ -104786,6 +105890,7 @@ Create Template from Scratch
|
|
|
104786
105890
|
]);
|
|
104787
105891
|
return {
|
|
104788
105892
|
metadataReader,
|
|
105893
|
+
estimationDefaults: estimationDefaults2,
|
|
104789
105894
|
fieldSchemas: taskSchemas,
|
|
104790
105895
|
filterCtx: {
|
|
104791
105896
|
workItemTypes: liveWorkItemTypes,
|
|
@@ -104812,6 +105917,7 @@ Create Template from Scratch
|
|
|
104812
105917
|
let filterCtx;
|
|
104813
105918
|
let fieldSchemas;
|
|
104814
105919
|
let adapterForWizard;
|
|
105920
|
+
let estimationDefaults;
|
|
104815
105921
|
const wasAlreadyConnected = connectionSettled;
|
|
104816
105922
|
const connectSpinner = createManagedSpinner();
|
|
104817
105923
|
if (!wasAlreadyConnected)
|
|
@@ -104823,6 +105929,7 @@ Create Template from Scratch
|
|
|
104823
105929
|
filterCtx = conn.filterCtx;
|
|
104824
105930
|
fieldSchemas = conn.fieldSchemas;
|
|
104825
105931
|
adapterForWizard = conn.metadataReader;
|
|
105932
|
+
estimationDefaults = conn.estimationDefaults;
|
|
104826
105933
|
} catch (err) {
|
|
104827
105934
|
if (!wasAlreadyConnected)
|
|
104828
105935
|
connectSpinner.stop("Connection failed");
|
|
@@ -104870,9 +105977,15 @@ Create Template from Scratch
|
|
|
104870
105977
|
printStep(source_default.blue(`
|
|
104871
105978
|
[${currentStep}/${totalSteps}] Estimation Settings`));
|
|
104872
105979
|
printStep(source_default.gray("████░░"));
|
|
104873
|
-
printStep(source_default.gray(`Tip: Choose how
|
|
105980
|
+
printStep(source_default.gray(`Tip: Choose how Story Estimates turn into Task hours and where they are written
|
|
104874
105981
|
`));
|
|
104875
|
-
const
|
|
105982
|
+
const estimationCtx = {
|
|
105983
|
+
storyFields: storyFieldSchemas.length ? storyFieldSchemas : await adapterForWizard.getFieldSchemas(),
|
|
105984
|
+
workItemTypes: filterCtx.workItemTypes,
|
|
105985
|
+
defaults: estimationDefaults,
|
|
105986
|
+
getTaskFields: (type) => adapterForWizard.getFieldSchemas(type)
|
|
105987
|
+
};
|
|
105988
|
+
const { estimation, taskType } = await configureEstimation(undefined, estimationCtx);
|
|
104876
105989
|
currentStep++;
|
|
104877
105990
|
printStep(source_default.blue(`
|
|
104878
105991
|
[${currentStep}/${totalSteps}] Validation Rules (Optional)`));
|
|
@@ -104912,6 +106025,7 @@ Create Template from Scratch
|
|
|
104912
106025
|
created: new Date().toISOString(),
|
|
104913
106026
|
filter: filterConfig,
|
|
104914
106027
|
tasks,
|
|
106028
|
+
...taskType ? { taskType } : {},
|
|
104915
106029
|
estimation,
|
|
104916
106030
|
validation,
|
|
104917
106031
|
metadata
|
|
@@ -104925,7 +106039,8 @@ Create Template from Scratch
|
|
|
104925
106039
|
filterCtx,
|
|
104926
106040
|
fieldSchemas,
|
|
104927
106041
|
storyFieldSchemas,
|
|
104928
|
-
workItemType
|
|
106042
|
+
workItemType,
|
|
106043
|
+
estimationCtx
|
|
104929
106044
|
};
|
|
104930
106045
|
const confirmed = await previewTemplate(template, wizardCtx);
|
|
104931
106046
|
if (!confirmed) {
|
|
@@ -105568,6 +106683,7 @@ init_dist6();
|
|
|
105568
106683
|
init_logger2();
|
|
105569
106684
|
init_estimation_distribution();
|
|
105570
106685
|
init_custom_field_verifier();
|
|
106686
|
+
init_schema2();
|
|
105571
106687
|
init_template_library();
|
|
105572
106688
|
init_errors();
|
|
105573
106689
|
init_source();
|
|
@@ -105711,7 +106827,9 @@ function createValidateCommandDeps(depsOptions) {
|
|
|
105711
106827
|
strict: options.strict === true,
|
|
105712
106828
|
platform: {
|
|
105713
106829
|
getFieldSchemas: (workItemType) => metadataReader.getFieldSchemas(workItemType),
|
|
105714
|
-
listSavedQueries: (folder) => savedQueryReader.listSavedQueries(folder)
|
|
106830
|
+
listSavedQueries: (folder) => savedQueryReader.listSavedQueries(folder),
|
|
106831
|
+
getEstimationDefaults: () => adapter.getEstimationDefaults(),
|
|
106832
|
+
getWorkItemTypes: adapter.getWorkItemTypes?.bind(adapter)
|
|
105715
106833
|
}
|
|
105716
106834
|
},
|
|
105717
106835
|
connectionWarnings: []
|
|
@@ -105800,7 +106918,7 @@ function getTemplateSummary(template) {
|
|
|
105800
106918
|
const totalPercent = totalUnconditionalEstimationPercent(template.tasks);
|
|
105801
106919
|
return {
|
|
105802
106920
|
name: template.name,
|
|
105803
|
-
tasks: template.tasks
|
|
106921
|
+
tasks: generatedTaskCount(template.tasks),
|
|
105804
106922
|
totalEstimation: `${totalPercent}%`
|
|
105805
106923
|
};
|
|
105806
106924
|
}
|