@sppg2001/atomize 4.0.1 → 4.1.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,30 @@ var init_errors = __esm(() => {
|
|
|
178
178
|
};
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// ../atomize-schema/src/estimation-fields.ts
|
|
182
|
+
function matchesFieldPattern(referenceName, patterns = []) {
|
|
183
|
+
return patterns.some((pattern) => pattern.endsWith(".*") ? referenceName.startsWith(pattern.slice(0, -1)) : referenceName === pattern);
|
|
184
|
+
}
|
|
185
|
+
function isPlatformExcluded(field, rules) {
|
|
186
|
+
return field.referenceName.startsWith("System.") || matchesFieldPattern(field.referenceName, rules?.nonEstimateFields);
|
|
187
|
+
}
|
|
188
|
+
function isEstimateSourceField(field, rules) {
|
|
189
|
+
if (field.isReadOnly)
|
|
190
|
+
return false;
|
|
191
|
+
if (rules?.storyEstimateFields?.includes(field.referenceName))
|
|
192
|
+
return true;
|
|
193
|
+
if (isPlatformExcluded(field, rules))
|
|
194
|
+
return false;
|
|
195
|
+
return field.type === "integer" || field.type === "decimal" || field.type === "string" && !field.isMultiline;
|
|
196
|
+
}
|
|
197
|
+
function isEstimateTargetField(field, rules) {
|
|
198
|
+
return !field.isReadOnly && !field.isPicklist && (field.type === "integer" || field.type === "decimal") && !isPlatformExcluded(field, rules);
|
|
199
|
+
}
|
|
200
|
+
function sortEstimateFields(fields) {
|
|
201
|
+
const custom = (field) => Number(field.isCustom ?? field.referenceName.startsWith("Custom."));
|
|
202
|
+
return [...fields].sort((a, b) => custom(b) - custom(a) || a.name.localeCompare(b.name));
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
// ../atomize-schema/src/estimation-normalizer.ts
|
|
182
206
|
function normalizeEstimationPercentages(items, options = {}) {
|
|
183
207
|
const { skipIfAlreadyNormalized = true, tolerance = 0.01, targetTotal = 100 } = options;
|
|
@@ -20065,6 +20089,25 @@ var init_zod = __esm(() => {
|
|
|
20065
20089
|
});
|
|
20066
20090
|
|
|
20067
20091
|
// ../atomize-schema/src/schema.ts
|
|
20092
|
+
function lookupTableEstimate(value, table, multipliers = false) {
|
|
20093
|
+
const text = String(value).trim();
|
|
20094
|
+
if (Object.hasOwn(table, text))
|
|
20095
|
+
return table[text];
|
|
20096
|
+
if (!multipliers)
|
|
20097
|
+
return;
|
|
20098
|
+
const candidates = Object.keys(table).filter((key) => key !== "" && text.endsWith(key)).sort((a, b) => b.length - a.length);
|
|
20099
|
+
for (const key of candidates) {
|
|
20100
|
+
const prefix = text.slice(0, text.length - key.length).trim();
|
|
20101
|
+
if (NUMBER_PATTERN.test(prefix))
|
|
20102
|
+
return Number(prefix) * table[key];
|
|
20103
|
+
}
|
|
20104
|
+
return;
|
|
20105
|
+
}
|
|
20106
|
+
function isConvertibleStoryEstimate(value, conversion) {
|
|
20107
|
+
if (conversion?.table)
|
|
20108
|
+
return lookupTableEstimate(value, conversion.table, conversion.multipliers) !== undefined;
|
|
20109
|
+
return typeof value === "number" || value.trim() !== "" && Number.isFinite(Number(value));
|
|
20110
|
+
}
|
|
20068
20111
|
function ownPercentRange(task) {
|
|
20069
20112
|
const candidates = [task.estimationPercent ?? 0, ...task.estimationPercentCondition?.map((rule) => rule.percent) ?? []];
|
|
20070
20113
|
return [Math.min(...candidates), Math.max(...candidates)];
|
|
@@ -20162,7 +20205,7 @@ function reportCircularDependencies(tasks, taskIndexById, ctx) {
|
|
|
20162
20205
|
});
|
|
20163
20206
|
}
|
|
20164
20207
|
}
|
|
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";
|
|
20208
|
+
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, MixinTemplateSchema, CURRENT_ITERATION = "@CurrentIteration", TEAM_AREAS = "@TeamAreas";
|
|
20166
20209
|
var init_schema = __esm(() => {
|
|
20167
20210
|
init_zod();
|
|
20168
20211
|
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 +20262,7 @@ var init_schema = __esm(() => {
|
|
|
20219
20262
|
});
|
|
20220
20263
|
FilterCriteriaSchema = exports_external.object({
|
|
20221
20264
|
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(),
|
|
20265
|
+
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
20266
|
states: exports_external.array(exports_external.string()).describe("Story states to target (e.g. ['Active', 'New']).").optional(),
|
|
20224
20267
|
statesExclude: exports_external.array(exports_external.string()).describe("Story states to exclude from selection.").optional(),
|
|
20225
20268
|
statesWereEver: exports_external.array(exports_external.string()).describe("Include stories that were ever in any of these states.").optional(),
|
|
@@ -20276,14 +20319,42 @@ var init_schema = __esm(() => {
|
|
|
20276
20319
|
return;
|
|
20277
20320
|
})
|
|
20278
20321
|
}).strict();
|
|
20322
|
+
NUMBER_PATTERN = /^(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
20323
|
+
EstimationConversionSchema = exports_external.object({
|
|
20324
|
+
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(),
|
|
20325
|
+
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(),
|
|
20326
|
+
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()
|
|
20327
|
+
}).strict().refine((conversion) => conversion.factor === undefined !== (conversion.table === undefined), {
|
|
20328
|
+
message: "conversion must set exactly one of factor or table"
|
|
20329
|
+
}).refine((conversion) => !conversion.multipliers || conversion.table !== undefined, {
|
|
20330
|
+
message: "conversion.multipliers requires conversion.table",
|
|
20331
|
+
path: ["multipliers"]
|
|
20332
|
+
}).refine((conversion) => !conversion.multipliers || !conversion.table || !Object.keys(conversion.table).every((key) => NUMBER_PATTERN.test(key)), {
|
|
20333
|
+
message: "conversion.multipliers cannot be used when every table key is numeric: '25' would be ambiguous between the key 25 and 2 × 5",
|
|
20334
|
+
path: ["multipliers"]
|
|
20335
|
+
}).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
20336
|
EstimationConfigSchema = exports_external.object({
|
|
20280
20337
|
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("
|
|
20338
|
+
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(),
|
|
20339
|
+
conversion: EstimationConversionSchema.optional(),
|
|
20340
|
+
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(),
|
|
20341
|
+
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
20342
|
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
|
-
|
|
20343
|
+
minimumTaskEstimate: exports_external.number().describe("Minimum Task Estimate any single task can receive after rounding, in the Task's unit.").optional(),
|
|
20344
|
+
minimumTaskPoints: exports_external.number().describe("Deprecated: use minimumTaskEstimate. Still honoured when minimumTaskEstimate is absent.").meta({ deprecated: true }).optional(),
|
|
20345
|
+
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(),
|
|
20346
|
+
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()
|
|
20347
|
+
}).strict().superRefine((config2, ctx) => {
|
|
20348
|
+
if (config2.ifParentHasNoEstimation !== "use-default" || config2.defaultParentEstimation === undefined)
|
|
20349
|
+
return;
|
|
20350
|
+
if (isConvertibleStoryEstimate(config2.defaultParentEstimation, config2.conversion))
|
|
20351
|
+
return;
|
|
20352
|
+
ctx.addIssue({
|
|
20353
|
+
code: "custom",
|
|
20354
|
+
path: ["defaultParentEstimation"],
|
|
20355
|
+
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`
|
|
20356
|
+
});
|
|
20357
|
+
});
|
|
20287
20358
|
ValidationModeSchema = exports_external.enum(["strict", "lenient"]).describe("'strict' fails on any validation warning; 'lenient' allows warnings to pass.");
|
|
20288
20359
|
ValidationConfigSchema = exports_external.object({
|
|
20289
20360
|
mode: ValidationModeSchema.describe("'strict' fails on any validation warning; 'lenient' allows warnings to pass.").optional(),
|
|
@@ -20326,6 +20397,7 @@ var init_schema = __esm(() => {
|
|
|
20326
20397
|
lastModified: exports_external.string().describe("ISO 8601 date of the last modification.").optional(),
|
|
20327
20398
|
filter: FilterCriteriaSchema.describe("Criteria used to select which stories this template applies to."),
|
|
20328
20399
|
tasks: exports_external.array(TaskDefinitionSchema).min(1, "At least one task is required").describe("Task definitions that will be generated from this template."),
|
|
20400
|
+
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
20401
|
estimation: EstimationConfigSchema.describe("Global estimation configuration for all tasks in this template.").optional(),
|
|
20330
20402
|
validation: ValidationConfigSchema.describe("Validation rules applied when this template is used.").optional(),
|
|
20331
20403
|
metadata: MetadataSchema.describe("Additional metadata for template discovery and documentation.").optional(),
|
|
@@ -20933,6 +21005,34 @@ var init_story_batch_processor = __esm(() => {
|
|
|
20933
21005
|
init_errors();
|
|
20934
21006
|
});
|
|
20935
21007
|
|
|
21008
|
+
// ../atomize-core/src/core/estimation-field-mapping.ts
|
|
21009
|
+
function resolveEstimationFieldMapping(defaults, overrides = {}) {
|
|
21010
|
+
const storySource = overrides.source ? { kind: "override", field: overrides.source } : { kind: "default", fields: defaults.storyEstimateFields };
|
|
21011
|
+
const taskTarget = overrides.targetFields?.length ? { kind: "override", fields: [...overrides.targetFields] } : { kind: "default", fields: defaults.taskEstimateFields };
|
|
21012
|
+
return {
|
|
21013
|
+
storySource,
|
|
21014
|
+
taskWorkItemType: overrides.taskType ?? defaults.taskWorkItemType,
|
|
21015
|
+
taskTarget,
|
|
21016
|
+
unitLabel: taskTarget.kind === "default" ? defaults.unitLabel : undefined
|
|
21017
|
+
};
|
|
21018
|
+
}
|
|
21019
|
+
function readStoryEstimate(story, source) {
|
|
21020
|
+
const value = source.kind === "default" ? story.estimation : story.customFields?.[source.field];
|
|
21021
|
+
return isStoryEstimate(value) ? value : undefined;
|
|
21022
|
+
}
|
|
21023
|
+
function numericStoryEstimate(value) {
|
|
21024
|
+
if (typeof value === "number")
|
|
21025
|
+
return Number.isFinite(value) ? value : undefined;
|
|
21026
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
21027
|
+
const parsed = Number(value);
|
|
21028
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
21029
|
+
}
|
|
21030
|
+
return;
|
|
21031
|
+
}
|
|
21032
|
+
function isStoryEstimate(value) {
|
|
21033
|
+
return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim() !== "";
|
|
21034
|
+
}
|
|
21035
|
+
|
|
20936
21036
|
// ../atomize-core/src/core/condition-evaluator.ts
|
|
20937
21037
|
function extractStandardFieldRefs(condition) {
|
|
20938
21038
|
const refs = [];
|
|
@@ -20966,22 +21066,29 @@ function extractCustomFieldRefs(condition) {
|
|
|
20966
21066
|
}
|
|
20967
21067
|
|
|
20968
21068
|
class ConditionEvaluator {
|
|
20969
|
-
evaluateCondition(condition, story) {
|
|
21069
|
+
evaluateCondition(condition, story, storyEstimateSource) {
|
|
20970
21070
|
if (!condition)
|
|
20971
21071
|
return true;
|
|
20972
|
-
return this.evaluate(condition, story);
|
|
21072
|
+
return this.evaluate(condition, story, storyEstimateSource);
|
|
20973
21073
|
}
|
|
20974
|
-
evaluate(condition, story) {
|
|
21074
|
+
evaluate(condition, story, source) {
|
|
20975
21075
|
if ("all" in condition) {
|
|
20976
|
-
return condition.all.every((c2) => this.evaluate(c2, story));
|
|
21076
|
+
return condition.all.every((c2) => this.evaluate(c2, story, source));
|
|
20977
21077
|
}
|
|
20978
21078
|
if ("any" in condition) {
|
|
20979
|
-
return condition.any.some((c2) => this.evaluate(c2, story));
|
|
21079
|
+
return condition.any.some((c2) => this.evaluate(c2, story, source));
|
|
20980
21080
|
}
|
|
20981
21081
|
if ("customField" in condition) {
|
|
20982
21082
|
const fieldValue2 = story.customFields?.[condition.customField];
|
|
20983
21083
|
return this.applyOperator(fieldValue2, condition.operator, condition.value);
|
|
20984
21084
|
}
|
|
21085
|
+
if (condition.field === "estimation") {
|
|
21086
|
+
const estimate = readStoryEstimate(story, source ?? { kind: "default", fields: [] });
|
|
21087
|
+
if (estimate !== undefined && NUMERIC_OPERATORS.has(condition.operator) && numericStoryEstimate(estimate) === undefined) {
|
|
21088
|
+
throw new Error(`cannot compare the Story Estimate "${estimate}" with "${condition.operator}" because it is not numeric; use equals or not-equals for categories`);
|
|
21089
|
+
}
|
|
21090
|
+
return this.applyOperator(estimate, condition.operator, condition.value);
|
|
21091
|
+
}
|
|
20985
21092
|
const fieldValue = this.resolveField(condition.field, story);
|
|
20986
21093
|
return this.applyOperator(fieldValue, condition.operator, condition.value);
|
|
20987
21094
|
}
|
|
@@ -21052,8 +21159,69 @@ class ConditionEvaluator {
|
|
|
21052
21159
|
return compare(a2, b2);
|
|
21053
21160
|
}
|
|
21054
21161
|
}
|
|
21162
|
+
var NUMERIC_OPERATORS;
|
|
21055
21163
|
var init_condition_evaluator = __esm(() => {
|
|
21056
21164
|
init_logger();
|
|
21165
|
+
NUMERIC_OPERATORS = new Set(["gt", "lt", "gte", "lte"]);
|
|
21166
|
+
});
|
|
21167
|
+
|
|
21168
|
+
// ../atomize-core/src/templates/schema.ts
|
|
21169
|
+
var init_schema2 = __esm(() => {
|
|
21170
|
+
init_src();
|
|
21171
|
+
});
|
|
21172
|
+
|
|
21173
|
+
// ../atomize-core/src/core/estimation-conversion.ts
|
|
21174
|
+
function convertStoryEstimate(raw, conversion) {
|
|
21175
|
+
if (raw === undefined) {
|
|
21176
|
+
return { kind: "unresolvable", reason: "Story has no estimate" };
|
|
21177
|
+
}
|
|
21178
|
+
if (conversion?.table) {
|
|
21179
|
+
const total = lookupTableEstimate(raw, conversion.table, conversion.multipliers);
|
|
21180
|
+
if (total === undefined) {
|
|
21181
|
+
const suffix = conversion.multipliers ? " and is not a multiple of a table key" : "";
|
|
21182
|
+
return { kind: "unresolvable", reason: `Story Estimate "${String(raw).trim()}" is not in the conversion table${suffix}` };
|
|
21183
|
+
}
|
|
21184
|
+
return { kind: "resolved", total: roundAwayFloatNoise(total) };
|
|
21185
|
+
}
|
|
21186
|
+
const numeric = numericStoryEstimate(raw);
|
|
21187
|
+
if (numeric === undefined) {
|
|
21188
|
+
return {
|
|
21189
|
+
kind: "unresolvable",
|
|
21190
|
+
reason: `Story Estimate "${raw}" is not numeric and the Template declares no conversion for it`
|
|
21191
|
+
};
|
|
21192
|
+
}
|
|
21193
|
+
const factor = conversion?.factor ?? 1;
|
|
21194
|
+
return { kind: "resolved", total: roundAwayFloatNoise(numeric * factor) };
|
|
21195
|
+
}
|
|
21196
|
+
function roundAwayFloatNoise(value) {
|
|
21197
|
+
return Math.round(value * 1e9) / 1e9;
|
|
21198
|
+
}
|
|
21199
|
+
function resolveStoryEstimate(raw, config2) {
|
|
21200
|
+
const converted = convertStoryEstimate(raw, config2?.conversion);
|
|
21201
|
+
if (converted.kind === "resolved")
|
|
21202
|
+
return converted;
|
|
21203
|
+
switch (config2?.ifParentHasNoEstimation ?? "warn") {
|
|
21204
|
+
case "skip":
|
|
21205
|
+
return { kind: "skip", reason: converted.reason };
|
|
21206
|
+
case "use-default": {
|
|
21207
|
+
if (config2?.defaultParentEstimation === undefined) {
|
|
21208
|
+
return {
|
|
21209
|
+
kind: "blank",
|
|
21210
|
+
reason: `${converted.reason}, and no defaultParentEstimation is set`
|
|
21211
|
+
};
|
|
21212
|
+
}
|
|
21213
|
+
const fallback = convertStoryEstimate(config2.defaultParentEstimation, config2.conversion);
|
|
21214
|
+
if (fallback.kind === "resolved") {
|
|
21215
|
+
return { kind: "resolved", total: fallback.total, fallbackReason: converted.reason };
|
|
21216
|
+
}
|
|
21217
|
+
return { kind: "blank", reason: `${converted.reason}, and the default Story Estimate is unusable: ${fallback.reason}` };
|
|
21218
|
+
}
|
|
21219
|
+
default:
|
|
21220
|
+
return { kind: "blank", reason: converted.reason };
|
|
21221
|
+
}
|
|
21222
|
+
}
|
|
21223
|
+
var init_estimation_conversion = __esm(() => {
|
|
21224
|
+
init_schema2();
|
|
21057
21225
|
});
|
|
21058
21226
|
|
|
21059
21227
|
// ../atomize-core/src/utils/estimation-normalizer.ts
|
|
@@ -21075,7 +21243,9 @@ var init_estimation_normalizer = __esm(() => {
|
|
|
21075
21243
|
// ../atomize-core/src/core/estimation-distribution.ts
|
|
21076
21244
|
function distributeActiveTaskPercentages(tasks, options = {}) {
|
|
21077
21245
|
const totalBefore = totalEstimationPercent(tasks);
|
|
21078
|
-
const
|
|
21246
|
+
const mode = options.mode ?? "auto";
|
|
21247
|
+
const shouldNormalize = mode === "always" ? totalBefore !== 100 : mode === "never" ? false : totalBefore < 100 || totalBefore > 100 && options.forceNormalize === true;
|
|
21248
|
+
const normalized = shouldNormalize ? normalizeEstimationPercentages2(tasks, {
|
|
21079
21249
|
skipIfAlreadyNormalized: false,
|
|
21080
21250
|
enableLogging: options.enableLogging ?? true
|
|
21081
21251
|
}) : false;
|
|
@@ -21130,11 +21300,19 @@ class EstimationCalculator {
|
|
|
21130
21300
|
constructor(conditionEvaluator = new ConditionEvaluator) {
|
|
21131
21301
|
this.conditionEvaluator = conditionEvaluator;
|
|
21132
21302
|
}
|
|
21133
|
-
calculateTasksWithSkipped(story, connectUserEmail, templateTasks, estimationConfig, forceNormalize = false) {
|
|
21303
|
+
calculateTasksWithSkipped(story, connectUserEmail, templateTasks, estimationConfig, forceNormalize = false, fieldMapping) {
|
|
21134
21304
|
logger.debug(`EstimationCalculator: Calculating tasks for story ${story.id}`);
|
|
21135
|
-
const
|
|
21136
|
-
if (
|
|
21137
|
-
logger.warn(`Story ${story.id}
|
|
21305
|
+
const resolution = this.resolveParentEstimate(story, estimationConfig, fieldMapping);
|
|
21306
|
+
if (resolution.kind === "skip") {
|
|
21307
|
+
logger.warn(`Story ${story.id}: ${resolution.reason}. Skipping the Story.`);
|
|
21308
|
+
return {
|
|
21309
|
+
calculatedTasks: [],
|
|
21310
|
+
skippedTasks: [],
|
|
21311
|
+
unresolvedEstimate: { action: "skip", reason: resolution.reason }
|
|
21312
|
+
};
|
|
21313
|
+
}
|
|
21314
|
+
if (resolution.kind === "blank") {
|
|
21315
|
+
logger.warn(`Story ${story.id}: ${resolution.reason}. Task Estimates will be left blank.`);
|
|
21138
21316
|
}
|
|
21139
21317
|
const calculatedTasks = [];
|
|
21140
21318
|
const skippedTasks = [];
|
|
@@ -21143,7 +21321,7 @@ class EstimationCalculator {
|
|
|
21143
21321
|
if (templateTask.condition) {
|
|
21144
21322
|
let conditionMet;
|
|
21145
21323
|
try {
|
|
21146
|
-
conditionMet = this.conditionEvaluator.evaluateCondition(templateTask.condition, story);
|
|
21324
|
+
conditionMet = this.conditionEvaluator.evaluateCondition(templateTask.condition, story, fieldMapping?.storySource);
|
|
21147
21325
|
} catch (error62) {
|
|
21148
21326
|
const reason = `Condition evaluation error: ${getErrorMessage(error62)}`;
|
|
21149
21327
|
logger.error(`Skipping task "${templateTask.title}" - ${reason}`);
|
|
@@ -21158,25 +21336,47 @@ class EstimationCalculator {
|
|
|
21158
21336
|
}
|
|
21159
21337
|
logger.debug(`Task "${templateTask.title}" - condition met: ${JSON.stringify(templateTask.condition)}`);
|
|
21160
21338
|
}
|
|
21161
|
-
|
|
21339
|
+
let resolvedPercent;
|
|
21340
|
+
try {
|
|
21341
|
+
resolvedPercent = this.resolveEffectivePercent(templateTask, story, fieldMapping);
|
|
21342
|
+
} catch (error62) {
|
|
21343
|
+
const reason = `Condition evaluation error: ${getErrorMessage(error62)}`;
|
|
21344
|
+
logger.error(`Skipping task "${templateTask.title}" - ${reason}`);
|
|
21345
|
+
skippedTasks.push({ templateTask, reason });
|
|
21346
|
+
continue;
|
|
21347
|
+
}
|
|
21162
21348
|
const calculatedTask = this.buildCalculatedTask(templateTask, story, connectUserEmail, 0, resolvedPercent);
|
|
21163
21349
|
calculatedTasks.push(calculatedTask);
|
|
21164
21350
|
}
|
|
21351
|
+
if (fieldMapping) {
|
|
21352
|
+
for (const task of calculatedTasks) {
|
|
21353
|
+
task.workItemType = fieldMapping.taskWorkItemType;
|
|
21354
|
+
if (fieldMapping.taskTarget.kind === "override") {
|
|
21355
|
+
task.estimateFields = [...fieldMapping.taskTarget.fields];
|
|
21356
|
+
}
|
|
21357
|
+
}
|
|
21358
|
+
}
|
|
21165
21359
|
distributeActiveTaskPercentages(calculatedTasks, {
|
|
21166
21360
|
forceNormalize,
|
|
21167
|
-
enableLogging: true
|
|
21361
|
+
enableLogging: true,
|
|
21362
|
+
mode: estimationConfig?.normalize
|
|
21168
21363
|
});
|
|
21169
21364
|
for (const calculatedTask of calculatedTasks) {
|
|
21170
|
-
calculatedTask.estimation = this.calculateEstimation(
|
|
21171
|
-
logger.debug(`Calculated task: ${calculatedTask.title} = ${calculatedTask.estimation}
|
|
21365
|
+
calculatedTask.estimation = resolution.kind === "resolved" ? this.calculateEstimation(resolution.total, calculatedTask, estimationConfig) : calculatedTask.estimationFixed;
|
|
21366
|
+
logger.debug(`Calculated task: ${calculatedTask.title} = ${calculatedTask.estimation ?? "unestimated"} (${calculatedTask.estimationPercent}%)`);
|
|
21172
21367
|
}
|
|
21173
21368
|
logger.info(`EstimationCalculator: Calculated ${calculatedTasks.length} tasks, skipped ${skippedTasks.length} tasks for story ${story.id}`);
|
|
21174
|
-
return {
|
|
21369
|
+
return {
|
|
21370
|
+
calculatedTasks,
|
|
21371
|
+
skippedTasks,
|
|
21372
|
+
...resolution.kind === "blank" ? { unresolvedEstimate: { action: "blank", reason: resolution.reason } } : {},
|
|
21373
|
+
...resolution.kind === "resolved" && resolution.fallbackReason ? { defaultEstimateReason: resolution.fallbackReason } : {}
|
|
21374
|
+
};
|
|
21175
21375
|
}
|
|
21176
|
-
resolveEffectivePercent(task, story) {
|
|
21376
|
+
resolveEffectivePercent(task, story, fieldMapping) {
|
|
21177
21377
|
if (task.estimationPercentCondition?.length) {
|
|
21178
21378
|
for (const rule of task.estimationPercentCondition) {
|
|
21179
|
-
if (this.conditionEvaluator.evaluateCondition(rule.condition, story)) {
|
|
21379
|
+
if (this.conditionEvaluator.evaluateCondition(rule.condition, story, fieldMapping?.storySource)) {
|
|
21180
21380
|
logger.debug(`Task "${task.title}": conditional percent → ${rule.percent}%`);
|
|
21181
21381
|
return rule.percent;
|
|
21182
21382
|
}
|
|
@@ -21193,7 +21393,6 @@ class EstimationCalculator {
|
|
|
21193
21393
|
assignTo: this.resolveAssignment(templateTask.assignTo, story, connectUserEmail),
|
|
21194
21394
|
priority: templateTask.priority,
|
|
21195
21395
|
activity: templateTask.activity,
|
|
21196
|
-
completedWork: 0,
|
|
21197
21396
|
iteration: story.iteration,
|
|
21198
21397
|
areaPath: story.areaPath,
|
|
21199
21398
|
customFields: this.interpolateCustomFields(templateTask.customFields, story),
|
|
@@ -21202,6 +21401,10 @@ class EstimationCalculator {
|
|
|
21202
21401
|
estimationFixed: templateTask.estimationFixed
|
|
21203
21402
|
};
|
|
21204
21403
|
}
|
|
21404
|
+
resolveParentEstimate(story, config2, fieldMapping) {
|
|
21405
|
+
const raw = readStoryEstimate(story, fieldMapping?.storySource ?? { kind: "default", fields: [] });
|
|
21406
|
+
return resolveStoryEstimate(raw, config2);
|
|
21407
|
+
}
|
|
21205
21408
|
calculateEstimation(parentEstimation, task, config2) {
|
|
21206
21409
|
if (task.estimationFixed !== undefined) {
|
|
21207
21410
|
return task.estimationFixed;
|
|
@@ -21209,7 +21412,7 @@ class EstimationCalculator {
|
|
|
21209
21412
|
if (task.estimationPercent !== undefined) {
|
|
21210
21413
|
const rawEstimation = parentEstimation * task.estimationPercent / 100;
|
|
21211
21414
|
const rounded = this.roundEstimation(rawEstimation, config2?.rounding || "none");
|
|
21212
|
-
const minimum = config2?.minimumTaskPoints
|
|
21415
|
+
const minimum = config2?.minimumTaskEstimate ?? config2?.minimumTaskPoints ?? 0;
|
|
21213
21416
|
return Math.max(rounded, minimum);
|
|
21214
21417
|
}
|
|
21215
21418
|
if (task.estimationFormula) {
|
|
@@ -21252,8 +21455,9 @@ class EstimationCalculator {
|
|
|
21252
21455
|
calculateTotalEstimation(tasks) {
|
|
21253
21456
|
return tasks.reduce((sum, task) => sum + (task.estimation || 0), 0);
|
|
21254
21457
|
}
|
|
21255
|
-
getEstimationSummary(story, tasks) {
|
|
21256
|
-
const
|
|
21458
|
+
getEstimationSummary(story, tasks, config2, fieldMapping) {
|
|
21459
|
+
const resolution = this.resolveParentEstimate(story, config2, fieldMapping);
|
|
21460
|
+
const storyEstimation = resolution.kind === "resolved" ? resolution.total : 0;
|
|
21257
21461
|
const totalTaskEstimation = this.calculateTotalEstimation(tasks);
|
|
21258
21462
|
const difference = storyEstimation - totalTaskEstimation;
|
|
21259
21463
|
const percentageUsed = storyEstimation > 0 ? totalTaskEstimation / storyEstimation * 100 : 0;
|
|
@@ -21264,13 +21468,16 @@ class EstimationCalculator {
|
|
|
21264
21468
|
percentageUsed
|
|
21265
21469
|
};
|
|
21266
21470
|
}
|
|
21267
|
-
validateEstimation(story, tasks) {
|
|
21471
|
+
validateEstimation(story, tasks, config2, fieldMapping) {
|
|
21268
21472
|
const warnings = [];
|
|
21269
|
-
|
|
21473
|
+
if (this.resolveParentEstimate(story, config2, fieldMapping).kind !== "resolved") {
|
|
21474
|
+
return { valid: true, warnings };
|
|
21475
|
+
}
|
|
21476
|
+
const summary = this.getEstimationSummary(story, tasks, config2, fieldMapping);
|
|
21270
21477
|
if (Math.abs(summary.difference) > 0.5) {
|
|
21271
21478
|
warnings.push(`Total task estimation (${summary.totalTaskEstimation}) differs from story estimation (${summary.storyEstimation}) by ${summary.difference}`);
|
|
21272
21479
|
}
|
|
21273
|
-
const zeroEstimations = tasks.filter((t2) =>
|
|
21480
|
+
const zeroEstimations = tasks.filter((t2) => t2.estimation === 0);
|
|
21274
21481
|
if (zeroEstimations.length > 0) {
|
|
21275
21482
|
warnings.push(`${zeroEstimations.length} task(s) have zero estimation: ${zeroEstimations.map((t2) => t2.title).join(", ")}`);
|
|
21276
21483
|
}
|
|
@@ -21285,6 +21492,7 @@ var init_estimation_calculator = __esm(() => {
|
|
|
21285
21492
|
init_logger();
|
|
21286
21493
|
init_errors();
|
|
21287
21494
|
init_condition_evaluator();
|
|
21495
|
+
init_estimation_conversion();
|
|
21288
21496
|
init_estimation_distribution();
|
|
21289
21497
|
});
|
|
21290
21498
|
|
|
@@ -21361,27 +21569,54 @@ var init_task_materializer = __esm(() => {
|
|
|
21361
21569
|
class StoryProcessor {
|
|
21362
21570
|
estimationCalculator;
|
|
21363
21571
|
taskMaterializer;
|
|
21572
|
+
estimationDefaults;
|
|
21364
21573
|
constructor(platform, estimationCalculator = new EstimationCalculator, taskMaterializer = new TaskMaterializer(platform)) {
|
|
21365
21574
|
this.estimationCalculator = estimationCalculator;
|
|
21366
21575
|
this.taskMaterializer = taskMaterializer;
|
|
21576
|
+
this.estimationDefaults = platform.getEstimationDefaults?.();
|
|
21367
21577
|
}
|
|
21368
21578
|
async process(story, orderedTasks, template, connectUserEmail, options, warnings) {
|
|
21369
21579
|
logger.info(`Processing: ${story.id} - ${story.title}`);
|
|
21370
|
-
const
|
|
21580
|
+
const fieldMapping = this.estimationDefaults ? resolveEstimationFieldMapping(this.estimationDefaults, {
|
|
21581
|
+
source: template.estimation?.source,
|
|
21582
|
+
taskType: template.taskType,
|
|
21583
|
+
targetFields: template.estimation?.targetFields
|
|
21584
|
+
}) : undefined;
|
|
21585
|
+
const { calculatedTasks, skippedTasks, unresolvedEstimate, defaultEstimateReason } = this.estimationCalculator.calculateTasksWithSkipped(story, connectUserEmail, orderedTasks, template.estimation, options.forceNormalize, fieldMapping);
|
|
21371
21586
|
logger.info(`Generated ${calculatedTasks.length} tasks, skipped ${skippedTasks.length} conditional tasks`);
|
|
21587
|
+
if (unresolvedEstimate?.action === "skip") {
|
|
21588
|
+
const skipReason = `${unresolvedEstimate.reason}; Story skipped (ifParentHasNoEstimation: skip)`;
|
|
21589
|
+
warnings.push(`${story.id}: ${skipReason}`);
|
|
21590
|
+
return {
|
|
21591
|
+
story,
|
|
21592
|
+
tasksCalculated: [],
|
|
21593
|
+
tasksCreated: [],
|
|
21594
|
+
tasksSkipped: [],
|
|
21595
|
+
success: true,
|
|
21596
|
+
...fieldMapping?.unitLabel ? { estimateUnit: fieldMapping.unitLabel } : {},
|
|
21597
|
+
...withStoryEstimate(readStoryEstimate(story, fieldMapping?.storySource ?? { kind: "default", fields: [] })),
|
|
21598
|
+
skipReason
|
|
21599
|
+
};
|
|
21600
|
+
}
|
|
21601
|
+
if (unresolvedEstimate?.action === "blank") {
|
|
21602
|
+
warnings.push(`${story.id}: ${unresolvedEstimate.reason}; Task Estimates left blank`);
|
|
21603
|
+
}
|
|
21604
|
+
if (defaultEstimateReason) {
|
|
21605
|
+
warnings.push(`${story.id}: ${defaultEstimateReason}; used defaultParentEstimation`);
|
|
21606
|
+
}
|
|
21372
21607
|
if (skippedTasks.length > 0) {
|
|
21373
21608
|
skippedTasks.forEach((skipped) => {
|
|
21374
21609
|
logger.debug(` Skipped: "${skipped.templateTask.title}" - ${skipped.reason}`);
|
|
21375
21610
|
});
|
|
21376
21611
|
}
|
|
21377
|
-
const validation = this.estimationCalculator.validateEstimation(story, calculatedTasks);
|
|
21612
|
+
const validation = this.estimationCalculator.validateEstimation(story, calculatedTasks, template.estimation, fieldMapping);
|
|
21378
21613
|
if (!validation.valid) {
|
|
21379
21614
|
validation.warnings.forEach((warning) => {
|
|
21380
21615
|
logger.warn(`${warning}`);
|
|
21381
21616
|
warnings.push(`${story.id}: ${warning}`);
|
|
21382
21617
|
});
|
|
21383
21618
|
}
|
|
21384
|
-
const estimationSummary = this.estimationCalculator.getEstimationSummary(story, calculatedTasks);
|
|
21619
|
+
const estimationSummary = this.estimationCalculator.getEstimationSummary(story, calculatedTasks, template.estimation, fieldMapping);
|
|
21385
21620
|
logger.debug("Estimation summary:", estimationSummary);
|
|
21386
21621
|
let tasksCreated = [];
|
|
21387
21622
|
if (options.dryRun) {
|
|
@@ -21397,10 +21632,15 @@ class StoryProcessor {
|
|
|
21397
21632
|
tasksCreated,
|
|
21398
21633
|
tasksSkipped: skippedTasks,
|
|
21399
21634
|
success: true,
|
|
21635
|
+
...fieldMapping?.unitLabel ? { estimateUnit: fieldMapping.unitLabel } : {},
|
|
21636
|
+
...withStoryEstimate(readStoryEstimate(story, fieldMapping?.storySource ?? { kind: "default", fields: [] })),
|
|
21400
21637
|
estimationSummary
|
|
21401
21638
|
};
|
|
21402
21639
|
}
|
|
21403
21640
|
}
|
|
21641
|
+
function withStoryEstimate(estimate) {
|
|
21642
|
+
return estimate === undefined ? {} : { storyEstimate: estimate };
|
|
21643
|
+
}
|
|
21404
21644
|
var init_story_processor = __esm(() => {
|
|
21405
21645
|
init_logger();
|
|
21406
21646
|
init_estimation_calculator();
|
|
@@ -79871,29 +80111,33 @@ class FieldSchemaService {
|
|
|
79871
80111
|
const allowedValues = witField.allowedValues && witField.allowedValues.length > 0 ? witField.allowedValues : undefined;
|
|
79872
80112
|
const isPicklist = !!(allowedValues ?? meta3?.isPicklist);
|
|
79873
80113
|
const isIdentity = meta3?.isIdentity === true || fieldType === import_WorkItemTrackingInterfaces.FieldType.Identity;
|
|
80114
|
+
const type = isIdentity ? "identity" : this.mapFieldType(fieldType);
|
|
79874
80115
|
return {
|
|
79875
80116
|
referenceName,
|
|
79876
80117
|
name: witField.name ?? meta3?.name ?? referenceName,
|
|
79877
|
-
type
|
|
80118
|
+
type,
|
|
79878
80119
|
isCustom: referenceName.startsWith("Custom."),
|
|
79879
80120
|
isReadOnly: meta3?.readOnly ?? false,
|
|
79880
80121
|
isMultiline: !isPicklist && (fieldType === import_WorkItemTrackingInterfaces.FieldType.Html || fieldType === import_WorkItemTrackingInterfaces.FieldType.PlainText),
|
|
79881
80122
|
isPicklist,
|
|
79882
|
-
allowedValues
|
|
80123
|
+
allowedValues,
|
|
80124
|
+
allowsCustomValues: isPicklist ? meta3?.isPicklistSuggested === true : type === "string"
|
|
79883
80125
|
};
|
|
79884
80126
|
}
|
|
79885
80127
|
mapWorkItemField(f) {
|
|
79886
80128
|
const referenceName = f.referenceName ?? "";
|
|
79887
80129
|
const isIdentity = f.isIdentity === true || f.type === import_WorkItemTrackingInterfaces.FieldType.Identity;
|
|
80130
|
+
const type = isIdentity ? "identity" : this.mapFieldType(f.type);
|
|
79888
80131
|
return {
|
|
79889
80132
|
referenceName,
|
|
79890
80133
|
name: f.name ?? referenceName,
|
|
79891
|
-
type
|
|
80134
|
+
type,
|
|
79892
80135
|
isCustom: referenceName.startsWith("Custom."),
|
|
79893
80136
|
isReadOnly: f.readOnly ?? false,
|
|
79894
80137
|
isMultiline: !f.isPicklist && (f.type === import_WorkItemTrackingInterfaces.FieldType.Html || f.type === import_WorkItemTrackingInterfaces.FieldType.PlainText),
|
|
79895
80138
|
isPicklist: f.isPicklist ?? false,
|
|
79896
|
-
allowedValues: undefined
|
|
80139
|
+
allowedValues: undefined,
|
|
80140
|
+
allowsCustomValues: f.isPicklist ? f.isPicklistSuggested === true : type === "string"
|
|
79897
80141
|
};
|
|
79898
80142
|
}
|
|
79899
80143
|
mapFieldType(fieldType) {
|
|
@@ -79920,6 +80164,33 @@ var init_azure_devops_field_schema_service = __esm(() => {
|
|
|
79920
80164
|
import_WorkItemTrackingInterfaces = __toESM(require_WorkItemTrackingInterfaces(), 1);
|
|
79921
80165
|
});
|
|
79922
80166
|
|
|
80167
|
+
// ../atomize-core/src/platforms/adapters/azure-devops/estimation-defaults.ts
|
|
80168
|
+
var AZURE_DEVOPS_ESTIMATION_DEFAULTS;
|
|
80169
|
+
var init_estimation_defaults = __esm(() => {
|
|
80170
|
+
AZURE_DEVOPS_ESTIMATION_DEFAULTS = {
|
|
80171
|
+
storyEstimateFields: [
|
|
80172
|
+
"Microsoft.VSTS.Scheduling.StoryPoints",
|
|
80173
|
+
"Microsoft.VSTS.Scheduling.Effort",
|
|
80174
|
+
"Microsoft.VSTS.Scheduling.Size",
|
|
80175
|
+
"Microsoft.VSTS.Scheduling.OriginalEstimate"
|
|
80176
|
+
],
|
|
80177
|
+
taskWorkItemType: "Task",
|
|
80178
|
+
taskEstimateFields: [
|
|
80179
|
+
"Microsoft.VSTS.Scheduling.RemainingWork",
|
|
80180
|
+
"Microsoft.VSTS.Scheduling.OriginalEstimate"
|
|
80181
|
+
],
|
|
80182
|
+
unitLabel: "hours",
|
|
80183
|
+
nonEstimateFields: [
|
|
80184
|
+
"Microsoft.VSTS.Common.*",
|
|
80185
|
+
"Microsoft.VSTS.Build.*",
|
|
80186
|
+
"Microsoft.VSTS.CMMI.*",
|
|
80187
|
+
"Microsoft.VSTS.CodeReview.*",
|
|
80188
|
+
"Microsoft.VSTS.Feedback.*",
|
|
80189
|
+
"Microsoft.VSTS.TCM.*"
|
|
80190
|
+
]
|
|
80191
|
+
};
|
|
80192
|
+
});
|
|
80193
|
+
|
|
79923
80194
|
// ../atomize-core/src/platforms/adapters/azure-devops/task-patch-builder.ts
|
|
79924
80195
|
function resolveDateMacro(value) {
|
|
79925
80196
|
if (typeof value !== "string")
|
|
@@ -79951,7 +80222,7 @@ function resolveDateMacro(value) {
|
|
|
79951
80222
|
base.setDate(base.getDate() + offsetDays);
|
|
79952
80223
|
return base.toISOString().slice(0, 10);
|
|
79953
80224
|
}
|
|
79954
|
-
function buildCreateTaskPatch(organizationUrl, parentId, task) {
|
|
80225
|
+
function buildCreateTaskPatch(organizationUrl, parentId, task, estimateWrite) {
|
|
79955
80226
|
return [
|
|
79956
80227
|
{
|
|
79957
80228
|
op: "add",
|
|
@@ -79965,25 +80236,12 @@ function buildCreateTaskPatch(organizationUrl, parentId, task) {
|
|
|
79965
80236
|
value: task.description
|
|
79966
80237
|
}
|
|
79967
80238
|
] : [],
|
|
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
|
-
] : [],
|
|
80239
|
+
...task.estimation !== undefined ? estimateWrite.fields.map((field) => ({
|
|
80240
|
+
op: "add",
|
|
80241
|
+
path: `/fields/${field}`,
|
|
80242
|
+
value: task.estimation
|
|
80243
|
+
})) : [],
|
|
80244
|
+
...estimateWrite.initializeCompletedWork ? [{ op: "add", path: "/fields/Microsoft.VSTS.Scheduling.CompletedWork", value: 0 }] : [],
|
|
79987
80245
|
...task.iteration ? [
|
|
79988
80246
|
{
|
|
79989
80247
|
op: "add",
|
|
@@ -80065,6 +80323,14 @@ var init_task_patch_builder = __esm(() => {
|
|
|
80065
80323
|
});
|
|
80066
80324
|
|
|
80067
80325
|
// ../atomize-core/src/platforms/adapters/azure-devops/work-item-mapper.ts
|
|
80326
|
+
function firstEstimate(fields) {
|
|
80327
|
+
for (const field of AZURE_DEVOPS_ESTIMATION_DEFAULTS.storyEstimateFields) {
|
|
80328
|
+
const value = fields[field];
|
|
80329
|
+
if ((typeof value === "number" || typeof value === "string") && value)
|
|
80330
|
+
return value;
|
|
80331
|
+
}
|
|
80332
|
+
return;
|
|
80333
|
+
}
|
|
80068
80334
|
function convertWorkItem(azureItem) {
|
|
80069
80335
|
const fields = azureItem.fields || {};
|
|
80070
80336
|
const predecessorIds = [];
|
|
@@ -80091,7 +80357,7 @@ function convertWorkItem(azureItem) {
|
|
|
80091
80357
|
type: fields["System.WorkItemType"],
|
|
80092
80358
|
state: fields["System.State"] || "",
|
|
80093
80359
|
assignedTo: fields["System.AssignedTo"]?.uniqueName || fields["System.AssignedTo"]?.displayName,
|
|
80094
|
-
estimation: fields
|
|
80360
|
+
estimation: firstEstimate(fields),
|
|
80095
80361
|
tags: fields["System.Tags"] ? fields["System.Tags"].split("; ") : [],
|
|
80096
80362
|
description: fields["System.Description"],
|
|
80097
80363
|
areaPath: fields["System.AreaPath"],
|
|
@@ -80115,10 +80381,8 @@ function hasChildRelations(workItem) {
|
|
|
80115
80381
|
}
|
|
80116
80382
|
return workItem.relations.some((rel) => rel.rel === "System.LinkTypes.Hierarchy-Forward");
|
|
80117
80383
|
}
|
|
80118
|
-
|
|
80119
|
-
|
|
80120
|
-
var init_schema2 = __esm(() => {
|
|
80121
|
-
init_src();
|
|
80384
|
+
var init_work_item_mapper = __esm(() => {
|
|
80385
|
+
init_estimation_defaults();
|
|
80122
80386
|
});
|
|
80123
80387
|
|
|
80124
80388
|
// ../atomize-core/src/platforms/adapters/azure-devops/work-item-query.ts
|
|
@@ -80383,6 +80647,7 @@ var import_WorkItemTrackingInterfaces2;
|
|
|
80383
80647
|
var init_work_item_retrieval = __esm(() => {
|
|
80384
80648
|
init_logger();
|
|
80385
80649
|
init_errors();
|
|
80650
|
+
init_work_item_mapper();
|
|
80386
80651
|
init_work_item_query();
|
|
80387
80652
|
import_WorkItemTrackingInterfaces2 = __toESM(require_WorkItemTrackingInterfaces(), 1);
|
|
80388
80653
|
});
|
|
@@ -80490,11 +80755,13 @@ class AzureDevOpsAdapter {
|
|
|
80490
80755
|
if (Number.isNaN(numericParentId)) {
|
|
80491
80756
|
throw new Error(`Invalid parent ID: ${parentId}`);
|
|
80492
80757
|
}
|
|
80493
|
-
const
|
|
80758
|
+
const defaults = this.getEstimationDefaults();
|
|
80759
|
+
const estimateWrite = task.estimateFields?.length ? { fields: task.estimateFields } : { fields: defaults.taskEstimateFields, initializeCompletedWork: true };
|
|
80760
|
+
const patchDocument = buildCreateTaskPatch(this.config.organizationUrl, numericParentId, task, estimateWrite);
|
|
80494
80761
|
if (!this.witApi) {
|
|
80495
80762
|
throw new UnknownError("Work Item Tracking API not initialized");
|
|
80496
80763
|
}
|
|
80497
|
-
const createdItem = await this.witApi.createWorkItem(undefined, patchDocument, this.config.project,
|
|
80764
|
+
const createdItem = await this.witApi.createWorkItem(undefined, patchDocument, this.config.project, task.workItemType ?? defaults.taskWorkItemType);
|
|
80498
80765
|
logger.info(`AzureDevOps: Created task ${createdItem.id}: ${task.title}`);
|
|
80499
80766
|
return convertWorkItem(createdItem);
|
|
80500
80767
|
} catch (error62) {
|
|
@@ -80531,6 +80798,9 @@ class AzureDevOpsAdapter {
|
|
|
80531
80798
|
logger.info(`AzureDevOps: Created ${createdTasks.length} of ${tasks.length} tasks`);
|
|
80532
80799
|
return createdTasks;
|
|
80533
80800
|
}
|
|
80801
|
+
getEstimationDefaults() {
|
|
80802
|
+
return AZURE_DEVOPS_ESTIMATION_DEFAULTS;
|
|
80803
|
+
}
|
|
80534
80804
|
getPlatformMetadata() {
|
|
80535
80805
|
return {
|
|
80536
80806
|
name: "Azure DevOps",
|
|
@@ -80743,7 +81013,9 @@ var init_azure_devops_adapter = __esm(() => {
|
|
|
80743
81013
|
init_logger();
|
|
80744
81014
|
init_errors();
|
|
80745
81015
|
init_azure_devops_field_schema_service();
|
|
81016
|
+
init_estimation_defaults();
|
|
80746
81017
|
init_task_patch_builder();
|
|
81018
|
+
init_work_item_mapper();
|
|
80747
81019
|
init_work_item_query();
|
|
80748
81020
|
init_work_item_retrieval();
|
|
80749
81021
|
azdev = __toESM(require_WebApi(), 1);
|
|
@@ -80995,7 +81267,7 @@ class MockPlatformAdapter {
|
|
|
80995
81267
|
const createdTask = {
|
|
80996
81268
|
id: `TASK-${this.taskIdCounter++}`,
|
|
80997
81269
|
title: task.title,
|
|
80998
|
-
type:
|
|
81270
|
+
type: task.workItemType ?? MOCK_ESTIMATION_DEFAULTS.taskWorkItemType,
|
|
80999
81271
|
state: "New",
|
|
81000
81272
|
assignedTo: task.assignTo,
|
|
81001
81273
|
estimation: task.estimation,
|
|
@@ -81063,6 +81335,9 @@ class MockPlatformAdapter {
|
|
|
81063
81335
|
logger.info(`MockPlatform: Deleted work item ${id}`);
|
|
81064
81336
|
return true;
|
|
81065
81337
|
}
|
|
81338
|
+
getEstimationDefaults() {
|
|
81339
|
+
return MOCK_ESTIMATION_DEFAULTS;
|
|
81340
|
+
}
|
|
81066
81341
|
getPlatformMetadata() {
|
|
81067
81342
|
return {
|
|
81068
81343
|
name: "Mock Platform",
|
|
@@ -81109,9 +81384,16 @@ class MockPlatformAdapter {
|
|
|
81109
81384
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81110
81385
|
}
|
|
81111
81386
|
}
|
|
81387
|
+
var MOCK_ESTIMATION_DEFAULTS;
|
|
81112
81388
|
var init_mock_adapter = __esm(() => {
|
|
81113
81389
|
init_logger();
|
|
81114
81390
|
init_mock_data();
|
|
81391
|
+
MOCK_ESTIMATION_DEFAULTS = {
|
|
81392
|
+
storyEstimateFields: ["estimation"],
|
|
81393
|
+
taskWorkItemType: "Task",
|
|
81394
|
+
taskEstimateFields: ["estimation"],
|
|
81395
|
+
unitLabel: "hours"
|
|
81396
|
+
};
|
|
81115
81397
|
});
|
|
81116
81398
|
|
|
81117
81399
|
// ../atomize-core/src/platforms/platform-factory.ts
|
|
@@ -88764,6 +89046,7 @@ function mergeTemplates(base, child) {
|
|
|
88764
89046
|
lastModified: child.lastModified ?? base.lastModified,
|
|
88765
89047
|
filter: child.filter ?? base.filter,
|
|
88766
89048
|
tasks: mergeTasks(base.tasks, child.tasks ?? []),
|
|
89049
|
+
taskType: child.taskType ?? base.taskType,
|
|
88767
89050
|
estimation: child.estimation ?? base.estimation,
|
|
88768
89051
|
validation: child.validation ?? base.validation,
|
|
88769
89052
|
metadata: child.metadata ?? base.metadata
|
|
@@ -89197,10 +89480,11 @@ class TemplateValidator {
|
|
|
89197
89480
|
const v2 = template.validation;
|
|
89198
89481
|
const total = template.tasks.reduce((sum, t2) => sum + (t2.estimationPercent ?? 0), 0);
|
|
89199
89482
|
const hasStrictRule = v2?.totalEstimationMustBe !== undefined || v2?.totalEstimationRange !== undefined;
|
|
89200
|
-
|
|
89483
|
+
const normalize = template.estimation?.normalize ?? "auto";
|
|
89484
|
+
if (!hasStrictRule && total > 100 && normalize !== "always") {
|
|
89201
89485
|
warnings.push({
|
|
89202
89486
|
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.`,
|
|
89487
|
+
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
89488
|
nonBlocking: true
|
|
89205
89489
|
});
|
|
89206
89490
|
}
|
|
@@ -89215,8 +89499,31 @@ class TemplateValidator {
|
|
|
89215
89499
|
this.validateTaskConditions(template, warnings);
|
|
89216
89500
|
this.validateTaskDependencies(template, warnings);
|
|
89217
89501
|
this.validateSavedQueryConflict(template, warnings);
|
|
89502
|
+
this.validateDeprecatedMinimumTaskPoints(template, warnings);
|
|
89218
89503
|
return warnings;
|
|
89219
89504
|
}
|
|
89505
|
+
validateDeprecatedMinimumTaskPoints(template, warnings) {
|
|
89506
|
+
const estimation = template.estimation;
|
|
89507
|
+
if (estimation?.minimumTaskPoints === undefined)
|
|
89508
|
+
return;
|
|
89509
|
+
if (estimation.minimumTaskEstimate === undefined) {
|
|
89510
|
+
warnings.push({
|
|
89511
|
+
path: "estimation.minimumTaskPoints",
|
|
89512
|
+
message: "minimumTaskPoints is deprecated; the minimum applies in the Task's unit, which may not be points.",
|
|
89513
|
+
suggestion: `Rename it to minimumTaskEstimate: ${estimation.minimumTaskPoints}.`,
|
|
89514
|
+
code: FixableWarningCode.DEPRECATED_MINIMUM_TASK_POINTS,
|
|
89515
|
+
nonBlocking: true
|
|
89516
|
+
});
|
|
89517
|
+
return;
|
|
89518
|
+
}
|
|
89519
|
+
warnings.push({
|
|
89520
|
+
path: "estimation.minimumTaskPoints",
|
|
89521
|
+
message: `Both minimumTaskEstimate and the deprecated minimumTaskPoints are set; minimumTaskEstimate (${estimation.minimumTaskEstimate}) is used and minimumTaskPoints (${estimation.minimumTaskPoints}) is ignored.`,
|
|
89522
|
+
suggestion: "Remove minimumTaskPoints.",
|
|
89523
|
+
code: FixableWarningCode.DUPLICATE_MINIMUM_TASK_ESTIMATE,
|
|
89524
|
+
nonBlocking: true
|
|
89525
|
+
});
|
|
89526
|
+
}
|
|
89220
89527
|
collectMixinWarnings(mixin) {
|
|
89221
89528
|
const warnings = [];
|
|
89222
89529
|
this.validateTaskDependencies(mixin, warnings);
|
|
@@ -89266,13 +89573,13 @@ class TemplateValidator {
|
|
|
89266
89573
|
const f = template.filter;
|
|
89267
89574
|
if (!f.savedQuery)
|
|
89268
89575
|
return;
|
|
89269
|
-
const hasStructuredFields = f.
|
|
89576
|
+
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
89577
|
if (hasStructuredFields) {
|
|
89271
89578
|
warnings.push({
|
|
89272
89579
|
path: "filter.savedQuery",
|
|
89273
89580
|
message: "savedQuery and structured filter fields are both set. Structured filter fields will be ignored — the saved query controls which items are returned.",
|
|
89274
89581
|
code: FixableWarningCode.SAVED_QUERY_WITH_STRUCTURED_FILTER,
|
|
89275
|
-
suggestion: "Remove
|
|
89582
|
+
suggestion: "Remove states, tags, area paths, etc. from the filter when using savedQuery. Keep workItemTypes: it declares which types the query returns."
|
|
89276
89583
|
});
|
|
89277
89584
|
}
|
|
89278
89585
|
}
|
|
@@ -89417,15 +89724,18 @@ var init_validator = __esm(() => {
|
|
|
89417
89724
|
FixableWarningCode = {
|
|
89418
89725
|
MISSING_TASK_ID: "MISSING_TASK_ID",
|
|
89419
89726
|
SAVED_QUERY_WITH_STRUCTURED_FILTER: "SAVED_QUERY_WITH_STRUCTURED_FILTER",
|
|
89420
|
-
SINGLE_LINE_FIELD_WITH_NEWLINES: "SINGLE_LINE_FIELD_WITH_NEWLINES"
|
|
89727
|
+
SINGLE_LINE_FIELD_WITH_NEWLINES: "SINGLE_LINE_FIELD_WITH_NEWLINES",
|
|
89728
|
+
DEPRECATED_MINIMUM_TASK_POINTS: "DEPRECATED_MINIMUM_TASK_POINTS",
|
|
89729
|
+
DUPLICATE_MINIMUM_TASK_ESTIMATE: "DUPLICATE_MINIMUM_TASK_ESTIMATE"
|
|
89421
89730
|
};
|
|
89422
89731
|
});
|
|
89423
89732
|
|
|
89424
89733
|
// ../atomize-core/src/templates/custom-field-verifier.ts
|
|
89425
|
-
async function verifyTemplateCustomFields(template, getSchemas) {
|
|
89734
|
+
async function verifyTemplateCustomFields(template, getSchemas, taskWorkItemType) {
|
|
89426
89735
|
const errors4 = [];
|
|
89427
89736
|
const warnings = [];
|
|
89428
|
-
const taskSchemas = await getSchemas(
|
|
89737
|
+
const taskSchemas = await getSchemas(taskWorkItemType);
|
|
89738
|
+
const scope = taskWorkItemType ? `work item type "${taskWorkItemType}"` : "this project";
|
|
89429
89739
|
const schemaByRef = new Map(taskSchemas.map((field) => [field.referenceName, field]));
|
|
89430
89740
|
for (let i2 = 0;i2 < template.tasks.length; i2++) {
|
|
89431
89741
|
const task = template.tasks[i2];
|
|
@@ -89437,7 +89747,7 @@ async function verifyTemplateCustomFields(template, getSchemas) {
|
|
|
89437
89747
|
if (!schema2) {
|
|
89438
89748
|
errors4.push({
|
|
89439
89749
|
path,
|
|
89440
|
-
message: `Field "${refName}" not found for
|
|
89750
|
+
message: `Field "${refName}" not found for ${scope}.`,
|
|
89441
89751
|
code: "CUSTOM_FIELD_NOT_FOUND"
|
|
89442
89752
|
});
|
|
89443
89753
|
continue;
|
|
@@ -89589,16 +89899,166 @@ var init_custom_field_verifier = __esm(() => {
|
|
|
89589
89899
|
init_validator();
|
|
89590
89900
|
});
|
|
89591
89901
|
|
|
89902
|
+
// ../atomize-core/src/templates/estimation-verifier.ts
|
|
89903
|
+
function hasEstimationMappingOverrides(template) {
|
|
89904
|
+
return !!(template.taskType || template.estimation?.source || template.estimation?.targetFields?.length);
|
|
89905
|
+
}
|
|
89906
|
+
async function verifyEstimationMapping(template, platform) {
|
|
89907
|
+
const errors4 = [];
|
|
89908
|
+
const warnings = [];
|
|
89909
|
+
if (!hasEstimationMappingOverrides(template))
|
|
89910
|
+
return { errors: errors4, warnings };
|
|
89911
|
+
const taskType = template.taskType ?? platform.getEstimationDefaults?.().taskWorkItemType;
|
|
89912
|
+
const nonEstimateFields = platform.getEstimationDefaults?.().nonEstimateFields ?? [];
|
|
89913
|
+
const taskTypeExists = await verifyTaskType(template, platform, errors4);
|
|
89914
|
+
const targetFields = template.estimation?.targetFields ?? [];
|
|
89915
|
+
if (targetFields.length > 0 && taskType && taskTypeExists) {
|
|
89916
|
+
const taskFields = byReference(await platform.getFieldSchemas(taskType));
|
|
89917
|
+
targetFields.forEach((ref, i2) => {
|
|
89918
|
+
const path = `estimation.targetFields[${i2}]`;
|
|
89919
|
+
const field = taskFields.get(ref);
|
|
89920
|
+
if (!field) {
|
|
89921
|
+
errors4.push({ path, message: `Field "${ref}" not found for work item type "${taskType}".`, code: "ESTIMATION_TARGET_FIELD_NOT_FOUND" });
|
|
89922
|
+
} else if (!NUMERIC_TYPES.has(field.type)) {
|
|
89923
|
+
errors4.push({
|
|
89924
|
+
path,
|
|
89925
|
+
message: `Field "${ref}" is a ${field.type} field; Task Estimates can only be written to numeric fields.`,
|
|
89926
|
+
code: "ESTIMATION_TARGET_FIELD_NOT_NUMERIC"
|
|
89927
|
+
});
|
|
89928
|
+
} else if (field.isPicklist) {
|
|
89929
|
+
errors4.push({
|
|
89930
|
+
path,
|
|
89931
|
+
message: `Field "${ref}" only accepts a fixed list of values, so calculated Task Estimates can't be written to it.`,
|
|
89932
|
+
code: "ESTIMATION_TARGET_FIELD_PICKLIST"
|
|
89933
|
+
});
|
|
89934
|
+
} else if (matchesFieldPattern(ref, nonEstimateFields)) {
|
|
89935
|
+
warnings.push({
|
|
89936
|
+
path,
|
|
89937
|
+
message: `Field "${ref}" holds a ranking or value, not effort; writing Task Estimates to it will overwrite that.`,
|
|
89938
|
+
suggestion: "Choose an effort field such as the platform's default estimate fields or a custom effort field."
|
|
89939
|
+
});
|
|
89940
|
+
}
|
|
89941
|
+
});
|
|
89942
|
+
}
|
|
89943
|
+
const source = template.estimation?.source;
|
|
89944
|
+
if (source) {
|
|
89945
|
+
const sourceField = await verifySourceField(template, source, platform, errors4);
|
|
89946
|
+
if (sourceField)
|
|
89947
|
+
checkSourceValues(template, sourceField, warnings);
|
|
89948
|
+
}
|
|
89949
|
+
return { errors: errors4, warnings };
|
|
89950
|
+
}
|
|
89951
|
+
async function verifyTaskType(template, platform, errors4) {
|
|
89952
|
+
if (!template.taskType || !platform.getWorkItemTypes)
|
|
89953
|
+
return true;
|
|
89954
|
+
const types = await platform.getWorkItemTypes();
|
|
89955
|
+
if (types.includes(template.taskType))
|
|
89956
|
+
return true;
|
|
89957
|
+
errors4.push({
|
|
89958
|
+
path: "taskType",
|
|
89959
|
+
message: `Work item type "${template.taskType}" does not exist in this project. Available types: ${types.join(", ")}.`,
|
|
89960
|
+
code: "ESTIMATION_TASK_TYPE_NOT_FOUND"
|
|
89961
|
+
});
|
|
89962
|
+
return false;
|
|
89963
|
+
}
|
|
89964
|
+
async function verifySourceField(template, source, platform, errors4) {
|
|
89965
|
+
const storyTypes = template.filter.workItemTypes?.length ? template.filter.workItemTypes : [undefined];
|
|
89966
|
+
let found;
|
|
89967
|
+
for (const storyType of storyTypes) {
|
|
89968
|
+
const field = byReference(await platform.getFieldSchemas(storyType)).get(source);
|
|
89969
|
+
if (field) {
|
|
89970
|
+
found ??= field;
|
|
89971
|
+
continue;
|
|
89972
|
+
}
|
|
89973
|
+
errors4.push({
|
|
89974
|
+
path: "estimation.source",
|
|
89975
|
+
message: storyType ? `Field "${source}" not found for work item type "${storyType}".` : `Field "${source}" not found in this project.`,
|
|
89976
|
+
code: "ESTIMATION_SOURCE_NOT_FOUND"
|
|
89977
|
+
});
|
|
89978
|
+
}
|
|
89979
|
+
return found;
|
|
89980
|
+
}
|
|
89981
|
+
function checkSourceValues(template, field, warnings) {
|
|
89982
|
+
const table = template.estimation?.conversion?.table;
|
|
89983
|
+
const multipliers = template.estimation?.conversion?.multipliers === true;
|
|
89984
|
+
const allowed = field.isPicklist ? field.allowedValues ?? [] : [];
|
|
89985
|
+
if (table && allowed.length > 0) {
|
|
89986
|
+
const keys = Object.keys(table);
|
|
89987
|
+
const uncovered = allowed.filter((value) => lookupTableEstimate(value, table, multipliers) === undefined);
|
|
89988
|
+
if (uncovered.length > 0) {
|
|
89989
|
+
warnings.push({
|
|
89990
|
+
path: "estimation.conversion.table",
|
|
89991
|
+
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.`,
|
|
89992
|
+
suggestion: "Add the missing values to conversion.table."
|
|
89993
|
+
});
|
|
89994
|
+
}
|
|
89995
|
+
const usedThroughMultiplier = (key) => multipliers && allowed.some((value) => value !== key && lookupTableEstimate(value, { [key]: 1 }, true) !== undefined);
|
|
89996
|
+
const unknown2 = keys.filter((key) => !allowed.includes(key) && !usedThroughMultiplier(key));
|
|
89997
|
+
if (unknown2.length > 0) {
|
|
89998
|
+
warnings.push({
|
|
89999
|
+
path: "estimation.conversion.table",
|
|
90000
|
+
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.`,
|
|
90001
|
+
suggestion: `Allowed values: ${listValues(allowed)}.`
|
|
90002
|
+
});
|
|
90003
|
+
}
|
|
90004
|
+
}
|
|
90005
|
+
if (field.type === "string" && field.allowsCustomValues) {
|
|
90006
|
+
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.";
|
|
90007
|
+
warnings.push({
|
|
90008
|
+
path: "estimation.source",
|
|
90009
|
+
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}`
|
|
90010
|
+
});
|
|
90011
|
+
}
|
|
90012
|
+
if (field.type === "string") {
|
|
90013
|
+
template.tasks.forEach((task, i2) => {
|
|
90014
|
+
const conditions = [
|
|
90015
|
+
[`tasks[${i2}].condition`, task.condition],
|
|
90016
|
+
...(task.estimationPercentCondition ?? []).map((rule, j2) => [`tasks[${i2}].estimationPercentCondition[${j2}].condition`, rule.condition])
|
|
90017
|
+
];
|
|
90018
|
+
for (const [path, condition] of conditions) {
|
|
90019
|
+
if (condition && usesNumericOperatorOnEstimation(condition)) {
|
|
90020
|
+
warnings.push({
|
|
90021
|
+
path,
|
|
90022
|
+
message: `This condition compares estimation numerically, but "${field.referenceName}" holds text values; it will skip the task for any non-numeric Story Estimate.`,
|
|
90023
|
+
suggestion: "Use equals or not-equals with the category values instead."
|
|
90024
|
+
});
|
|
90025
|
+
}
|
|
90026
|
+
}
|
|
90027
|
+
});
|
|
90028
|
+
}
|
|
90029
|
+
}
|
|
90030
|
+
function usesNumericOperatorOnEstimation(condition) {
|
|
90031
|
+
if ("all" in condition)
|
|
90032
|
+
return condition.all.some(usesNumericOperatorOnEstimation);
|
|
90033
|
+
if ("any" in condition)
|
|
90034
|
+
return condition.any.some(usesNumericOperatorOnEstimation);
|
|
90035
|
+
return "field" in condition && condition.field === "estimation" && NUMERIC_OPERATORS2.has(condition.operator);
|
|
90036
|
+
}
|
|
90037
|
+
function byReference(fields) {
|
|
90038
|
+
return new Map(fields.map((field) => [field.referenceName, field]));
|
|
90039
|
+
}
|
|
90040
|
+
function listValues(values) {
|
|
90041
|
+
return values.map((value) => `"${value}"`).join(", ");
|
|
90042
|
+
}
|
|
90043
|
+
var NUMERIC_TYPES, NUMERIC_OPERATORS2;
|
|
90044
|
+
var init_estimation_verifier = __esm(() => {
|
|
90045
|
+
init_schema2();
|
|
90046
|
+
NUMERIC_TYPES = new Set(["integer", "decimal"]);
|
|
90047
|
+
NUMERIC_OPERATORS2 = new Set(["gt", "lt", "gte", "lte"]);
|
|
90048
|
+
});
|
|
90049
|
+
|
|
89592
90050
|
// ../atomize-core/src/templates/project-verifier.ts
|
|
89593
90051
|
function analyzeTemplateProjectVerification(template) {
|
|
89594
90052
|
const customFieldTaskCount = template.tasks.filter((task) => task.customFields && Object.keys(task.customFields).length > 0).length;
|
|
89595
90053
|
const conditionFieldRefs = Array.from(new Set(template.tasks.flatMap((task) => task.condition ? extractCustomFieldRefs(task.condition) : [])));
|
|
89596
90054
|
const hasSavedQuery = !!(template.filter?.savedQuery?.id || template.filter?.savedQuery?.path);
|
|
90055
|
+
const hasEstimationMapping = hasEstimationMappingOverrides(template);
|
|
89597
90056
|
return {
|
|
89598
90057
|
customFieldTaskCount,
|
|
89599
90058
|
conditionFieldRefs,
|
|
89600
90059
|
hasSavedQuery,
|
|
89601
|
-
|
|
90060
|
+
hasEstimationMapping,
|
|
90061
|
+
needsOnlineVerification: customFieldTaskCount > 0 || conditionFieldRefs.length > 0 || hasSavedQuery || hasEstimationMapping
|
|
89602
90062
|
};
|
|
89603
90063
|
}
|
|
89604
90064
|
async function verifyTemplateProject(template, options) {
|
|
@@ -89615,12 +90075,28 @@ async function verifyTemplateProject(template, options) {
|
|
|
89615
90075
|
return result;
|
|
89616
90076
|
}
|
|
89617
90077
|
if ((requirements.customFieldTaskCount > 0 || requirements.conditionFieldRefs.length > 0) && options.platform?.getFieldSchemas) {
|
|
89618
|
-
const customFields = await verifyTemplateCustomFields(template, options.platform.getFieldSchemas.bind(options.platform));
|
|
90078
|
+
const customFields = await verifyTemplateCustomFields(template, options.platform.getFieldSchemas.bind(options.platform), template.taskType ?? options.platform.getEstimationDefaults?.().taskWorkItemType);
|
|
89619
90079
|
result.errors.push(...customFields.errors);
|
|
89620
90080
|
result.warnings.push(...customFields.warnings);
|
|
89621
90081
|
if (customFields.errors.length > 0)
|
|
89622
90082
|
result.valid = false;
|
|
89623
90083
|
}
|
|
90084
|
+
const platform = options.platform;
|
|
90085
|
+
if (requirements.hasEstimationMapping && platform?.getFieldSchemas) {
|
|
90086
|
+
const estimation = await verifyEstimationMapping(template, {
|
|
90087
|
+
getFieldSchemas: platform.getFieldSchemas.bind(platform),
|
|
90088
|
+
getWorkItemTypes: platform.getWorkItemTypes?.bind(platform),
|
|
90089
|
+
getEstimationDefaults: platform.getEstimationDefaults?.bind(platform)
|
|
90090
|
+
});
|
|
90091
|
+
result.errors.push(...estimation.errors);
|
|
90092
|
+
if (options.strict === true || template.validation?.mode === "strict") {
|
|
90093
|
+
result.errors.push(...estimation.warnings.map((w2) => ({ path: w2.path, message: w2.message, suggestion: w2.suggestion, code: "STRICT_MODE_WARNING" })));
|
|
90094
|
+
} else {
|
|
90095
|
+
result.warnings.push(...estimation.warnings);
|
|
90096
|
+
}
|
|
90097
|
+
if (result.errors.length > 0)
|
|
90098
|
+
result.valid = false;
|
|
90099
|
+
}
|
|
89624
90100
|
if (requirements.hasSavedQuery && options.platform?.listSavedQueries) {
|
|
89625
90101
|
const savedQuery = await verifySavedQuery(template, options.platform);
|
|
89626
90102
|
result.errors.push(...savedQuery.errors);
|
|
@@ -89676,6 +90152,7 @@ async function verifySavedQuery(template, platform) {
|
|
|
89676
90152
|
var init_project_verifier = __esm(() => {
|
|
89677
90153
|
init_condition_evaluator();
|
|
89678
90154
|
init_custom_field_verifier();
|
|
90155
|
+
init_estimation_verifier();
|
|
89679
90156
|
});
|
|
89680
90157
|
|
|
89681
90158
|
// ../atomize-core/src/templates/template-verification.ts
|
|
@@ -92800,6 +93277,25 @@ function assertNotCancelled(value) {
|
|
|
92800
93277
|
}
|
|
92801
93278
|
return value;
|
|
92802
93279
|
}
|
|
93280
|
+
function parseConversionTable(input2) {
|
|
93281
|
+
const entries = input2.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
93282
|
+
if (entries.length === 0)
|
|
93283
|
+
return "Enter at least one entry, e.g. S=2, M=4, L=5";
|
|
93284
|
+
const table = {};
|
|
93285
|
+
for (const entry of entries) {
|
|
93286
|
+
const separator = entry.lastIndexOf("=");
|
|
93287
|
+
const key = separator > 0 ? entry.slice(0, separator).trim() : "";
|
|
93288
|
+
const value = Number(entry.slice(separator + 1).trim());
|
|
93289
|
+
if (!key || separator === entry.length - 1 || Number.isNaN(value))
|
|
93290
|
+
return `"${entry}" is not in the form value=amount`;
|
|
93291
|
+
if (value < 0)
|
|
93292
|
+
return `"${key}" cannot have a negative amount`;
|
|
93293
|
+
if (Object.hasOwn(table, key))
|
|
93294
|
+
return `"${key}" appears more than once`;
|
|
93295
|
+
table[key] = value;
|
|
93296
|
+
}
|
|
93297
|
+
return table;
|
|
93298
|
+
}
|
|
92803
93299
|
async function selectOrAutocomplete(opts) {
|
|
92804
93300
|
const count = opts.thresholdCount ?? opts.options.length;
|
|
92805
93301
|
if (count > AUTOCOMPLETE_THRESHOLD) {
|
|
@@ -92888,6 +93384,25 @@ var init_prompt_utilities = __esm(() => {
|
|
|
92888
93384
|
return required2;
|
|
92889
93385
|
return Validators.maxLength(fieldName, maxLength)(input2);
|
|
92890
93386
|
},
|
|
93387
|
+
positiveNumber: (fieldName) => (input2) => {
|
|
93388
|
+
const num = Number(input2);
|
|
93389
|
+
if (!input2 || input2.trim() === "" || Number.isNaN(num))
|
|
93390
|
+
return `${fieldName} must be a number`;
|
|
93391
|
+
if (num <= 0)
|
|
93392
|
+
return `${fieldName} must be greater than 0`;
|
|
93393
|
+
return;
|
|
93394
|
+
},
|
|
93395
|
+
fieldReferenceName: (input2) => {
|
|
93396
|
+
if (!input2 || input2.trim() === "")
|
|
93397
|
+
return "Field reference name is required";
|
|
93398
|
+
if (/\s/.test(input2.trim()))
|
|
93399
|
+
return "Use the field's reference name (e.g. Custom.TShirtSize), which has no spaces";
|
|
93400
|
+
return;
|
|
93401
|
+
},
|
|
93402
|
+
conversionTable: (input2) => {
|
|
93403
|
+
const parsed = parseConversionTable(input2 ?? "");
|
|
93404
|
+
return typeof parsed === "string" ? parsed : undefined;
|
|
93405
|
+
},
|
|
92891
93406
|
estimationPercent: (input2) => {
|
|
92892
93407
|
const num = Number(input2);
|
|
92893
93408
|
if (Number.isNaN(num))
|
|
@@ -93201,9 +93716,19 @@ var init_profile_resolver = __esm(() => {
|
|
|
93201
93716
|
init_keychain_service();
|
|
93202
93717
|
});
|
|
93203
93718
|
|
|
93719
|
+
// ../atomize-core/src/services/template/estimation-grounding.ts
|
|
93720
|
+
function isEstimateCapableField(field, rules) {
|
|
93721
|
+
return isEstimateSourceField(field, rules);
|
|
93722
|
+
}
|
|
93723
|
+
var init_estimation_grounding = __esm(() => {
|
|
93724
|
+
init_src();
|
|
93725
|
+
init_src();
|
|
93726
|
+
});
|
|
93727
|
+
|
|
93204
93728
|
// src/cli/commands/template/template-wizard-helper.command.ts
|
|
93205
93729
|
var exports_template_wizard_helper_command = {};
|
|
93206
93730
|
__export(exports_template_wizard_helper_command, {
|
|
93731
|
+
renderConversionTable: () => renderConversionTable,
|
|
93207
93732
|
promptMixinRefs: () => promptMixinRefs,
|
|
93208
93733
|
configureValidation: () => configureValidation,
|
|
93209
93734
|
configureTemplateComposition: () => configureTemplateComposition,
|
|
@@ -93268,7 +93793,7 @@ async function configureFilter(ctx, defaults) {
|
|
|
93268
93793
|
initialValue: defaultFilterMode
|
|
93269
93794
|
}));
|
|
93270
93795
|
if (filterMode === "savedQuery") {
|
|
93271
|
-
return promptSavedQuery(ctx
|
|
93796
|
+
return promptSavedQuery(ctx, defaults);
|
|
93272
93797
|
}
|
|
93273
93798
|
const filter = {};
|
|
93274
93799
|
const workItemTypes = await promptWorkItemTypes(ctx, defaults?.workItemTypes);
|
|
@@ -93353,8 +93878,8 @@ async function configureFilter(ctx, defaults) {
|
|
|
93353
93878
|
}
|
|
93354
93879
|
return filter;
|
|
93355
93880
|
}
|
|
93356
|
-
async function promptSavedQuery(
|
|
93357
|
-
const sorted = [...
|
|
93881
|
+
async function promptSavedQuery(ctx, defaults) {
|
|
93882
|
+
const sorted = [...ctx.savedQueries].sort((a3, b3) => {
|
|
93358
93883
|
if (a3.isPublic !== b3.isPublic)
|
|
93359
93884
|
return a3.isPublic ? -1 : 1;
|
|
93360
93885
|
return a3.path.localeCompare(b3.path);
|
|
@@ -93368,16 +93893,21 @@ async function promptSavedQuery(queries) {
|
|
|
93368
93893
|
})),
|
|
93369
93894
|
placeholder: "Type to filter by path or name..."
|
|
93370
93895
|
});
|
|
93896
|
+
const workItemTypes = await promptWorkItemTypes(ctx, defaults?.workItemTypes, {
|
|
93897
|
+
message: "Which work item types does this query return? (used to look up their fields, not to filter)",
|
|
93898
|
+
required: true
|
|
93899
|
+
});
|
|
93371
93900
|
const excludeIfHasTasks = assertNotCancelled(await confirm({
|
|
93372
93901
|
message: "Exclude work items that already have tasks?",
|
|
93373
93902
|
initialValue: true
|
|
93374
93903
|
}));
|
|
93375
93904
|
return {
|
|
93376
93905
|
savedQuery: { id: selectedId.trim() },
|
|
93906
|
+
workItemTypes,
|
|
93377
93907
|
...excludeIfHasTasks && { excludeIfHasTasks: true }
|
|
93378
93908
|
};
|
|
93379
93909
|
}
|
|
93380
|
-
async function promptWorkItemTypes(ctx, defaults) {
|
|
93910
|
+
async function promptWorkItemTypes(ctx, defaults, options = {}) {
|
|
93381
93911
|
const defaultType = ctx.workItemTypes.includes("User Story") ? "User Story" : undefined;
|
|
93382
93912
|
const sorted = [
|
|
93383
93913
|
...defaultType ? [defaultType] : [],
|
|
@@ -93385,10 +93915,10 @@ async function promptWorkItemTypes(ctx, defaults) {
|
|
|
93385
93915
|
];
|
|
93386
93916
|
const initialValues = defaults && defaults.length > 0 ? defaults.filter((t3) => ctx.workItemTypes.includes(t3)) : defaultType ? [defaultType] : [];
|
|
93387
93917
|
const selected = assertNotCancelled(await multiselect({
|
|
93388
|
-
message: "Select work item types:",
|
|
93918
|
+
message: options.message ?? "Select work item types:",
|
|
93389
93919
|
options: sorted.map((t3) => ({ label: t3, value: t3 })),
|
|
93390
93920
|
initialValues,
|
|
93391
|
-
required: false
|
|
93921
|
+
required: options.required ?? false
|
|
93392
93922
|
}));
|
|
93393
93923
|
return selected;
|
|
93394
93924
|
}
|
|
@@ -93681,7 +94211,12 @@ async function promptDateFilter(selectMessage, customMessage, defaults) {
|
|
|
93681
94211
|
}
|
|
93682
94212
|
return choice;
|
|
93683
94213
|
}
|
|
93684
|
-
async function configureEstimation(defaults) {
|
|
94214
|
+
async function configureEstimation(defaults, ctx = {}, currentTaskType) {
|
|
94215
|
+
const source = await promptEstimationSource(defaults?.source, ctx);
|
|
94216
|
+
const sourceField = source ? ctx.storyFields?.find((field) => field.referenceName === source) : undefined;
|
|
94217
|
+
const conversion = await promptConversion(defaults?.conversion, sourceField);
|
|
94218
|
+
const sourceValues = sourceField?.isPicklist ? sourceField.allowedValues ?? [] : [];
|
|
94219
|
+
const { taskType, targetFields } = await promptEstimateTarget(currentTaskType, defaults?.targetFields, ctx);
|
|
93685
94220
|
const rounding = assertNotCancelled(await select({
|
|
93686
94221
|
message: "Rounding strategy:",
|
|
93687
94222
|
options: [
|
|
@@ -93692,23 +94227,267 @@ async function configureEstimation(defaults) {
|
|
|
93692
94227
|
],
|
|
93693
94228
|
initialValue: defaults?.rounding ?? "none"
|
|
93694
94229
|
}));
|
|
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
|
-
}
|
|
94230
|
+
const minimumTaskEstimateRaw = assertNotCancelled(await text({
|
|
94231
|
+
message: "Minimum Task Estimate, in the Task's unit (0 for no minimum):",
|
|
94232
|
+
initialValue: String(defaults?.minimumTaskEstimate ?? defaults?.minimumTaskPoints ?? 0),
|
|
94233
|
+
validate: Validators.nonNegative("Minimum Task Estimate")
|
|
93706
94234
|
}));
|
|
93707
|
-
|
|
94235
|
+
const normalize = assertNotCancelled(await select({
|
|
94236
|
+
message: "When the generated tasks' percentages don't add up to 100%:",
|
|
94237
|
+
options: [
|
|
94238
|
+
{ label: "Fill up to 100% (scale up below 100%, keep anything above)", value: "auto" },
|
|
94239
|
+
{ label: "Always scale to exactly 100%", value: "always" },
|
|
94240
|
+
{ label: "Use the percentages as written", value: "never" }
|
|
94241
|
+
],
|
|
94242
|
+
initialValue: defaults?.normalize ?? "auto"
|
|
94243
|
+
}));
|
|
94244
|
+
const policy = assertNotCancelled(await select({
|
|
94245
|
+
message: "When a Story's estimate is missing or can't be converted:",
|
|
94246
|
+
options: [
|
|
94247
|
+
{ label: "Create the tasks with blank estimates and warn", value: "warn" },
|
|
94248
|
+
{ label: "Skip the Story", value: "skip" },
|
|
94249
|
+
{ label: "Assume a default Story Estimate", value: "use-default" }
|
|
94250
|
+
],
|
|
94251
|
+
initialValue: defaults?.ifParentHasNoEstimation ?? "warn"
|
|
94252
|
+
}));
|
|
94253
|
+
const defaultParentEstimation = policy === "use-default" ? await promptDefaultStoryEstimate(defaults?.defaultParentEstimation, conversion, sourceValues) : undefined;
|
|
94254
|
+
const {
|
|
94255
|
+
source: _source,
|
|
94256
|
+
conversion: _conversion,
|
|
94257
|
+
targetFields: _targetFields,
|
|
94258
|
+
minimumTaskPoints: _minimumTaskPoints,
|
|
94259
|
+
defaultParentEstimation: _default3,
|
|
94260
|
+
ifParentHasNoEstimation: _policy,
|
|
94261
|
+
normalize: _normalize2,
|
|
94262
|
+
...kept
|
|
94263
|
+
} = defaults ?? {};
|
|
94264
|
+
const estimation = {
|
|
94265
|
+
...kept,
|
|
93708
94266
|
strategy: "percentage",
|
|
93709
94267
|
rounding,
|
|
93710
|
-
|
|
93711
|
-
|
|
94268
|
+
minimumTaskEstimate: Number(minimumTaskEstimateRaw) || undefined,
|
|
94269
|
+
...source ? { source } : {},
|
|
94270
|
+
...conversion ? { conversion } : {},
|
|
94271
|
+
...targetFields?.length ? { targetFields } : {},
|
|
94272
|
+
...normalize !== "auto" ? { normalize } : {},
|
|
94273
|
+
...policy !== "warn" ? { ifParentHasNoEstimation: policy } : {},
|
|
94274
|
+
...defaultParentEstimation !== undefined ? { defaultParentEstimation } : {}
|
|
94275
|
+
};
|
|
94276
|
+
if (estimation.minimumTaskEstimate === undefined)
|
|
94277
|
+
delete estimation.minimumTaskEstimate;
|
|
94278
|
+
return { estimation, ...taskType ? { taskType } : {} };
|
|
94279
|
+
}
|
|
94280
|
+
async function promptEstimationSource(current, ctx) {
|
|
94281
|
+
const rules = { storyEstimateFields: ctx.defaults?.storyEstimateFields ?? [], nonEstimateFields: ctx.defaults?.nonEstimateFields };
|
|
94282
|
+
const candidates = sortEstimateFields((ctx.storyFields ?? []).filter((field) => isEstimateCapableField(field, rules)));
|
|
94283
|
+
if (ctx.storyFields !== undefined) {
|
|
94284
|
+
const choice = assertNotCancelled(await select({
|
|
94285
|
+
message: "Which Story field holds the Story Estimate?",
|
|
94286
|
+
options: [
|
|
94287
|
+
{ label: "Platform default (story points, effort or size)", value: "" },
|
|
94288
|
+
...candidates.map((field) => ({
|
|
94289
|
+
label: `${field.name} (${field.referenceName})`,
|
|
94290
|
+
value: field.referenceName,
|
|
94291
|
+
hint: field.isPicklist ? `values: ${(field.allowedValues ?? []).join(", ")}` : field.type
|
|
94292
|
+
})),
|
|
94293
|
+
{ label: "Another field (enter its reference name)", value: CUSTOM_ENTRY }
|
|
94294
|
+
],
|
|
94295
|
+
initialValue: current && candidates.some((field) => field.referenceName === current) ? current : current ? CUSTOM_ENTRY : ""
|
|
94296
|
+
}));
|
|
94297
|
+
if (choice !== CUSTOM_ENTRY)
|
|
94298
|
+
return choice || undefined;
|
|
94299
|
+
}
|
|
94300
|
+
const typed = assertNotCancelled(await text({
|
|
94301
|
+
message: "Story field reference name holding the Story Estimate (leave blank for the platform default):",
|
|
94302
|
+
initialValue: current ?? "",
|
|
94303
|
+
validate: (input2) => input2 && input2.trim() !== "" ? Validators.fieldReferenceName(input2) : undefined
|
|
94304
|
+
}));
|
|
94305
|
+
return typed.trim() || undefined;
|
|
94306
|
+
}
|
|
94307
|
+
async function promptConversion(current, sourceField) {
|
|
94308
|
+
const kind = assertNotCancelled(await select({
|
|
94309
|
+
message: "How does the Story Estimate translate into Task hours?",
|
|
94310
|
+
options: [
|
|
94311
|
+
{ label: "One-to-one (use the number as it is)", value: "none" },
|
|
94312
|
+
{ label: "A factor (e.g. 1 point = 4 hours)", value: "factor" },
|
|
94313
|
+
{ label: "A table (e.g. t-shirt sizes, or points that don't scale linearly)", value: "table" }
|
|
94314
|
+
],
|
|
94315
|
+
initialValue: current?.table ? "table" : current?.factor !== undefined ? "factor" : sourceField?.isPicklist ? "table" : "none"
|
|
94316
|
+
}));
|
|
94317
|
+
if (kind === "none")
|
|
94318
|
+
return;
|
|
94319
|
+
if (kind === "factor") {
|
|
94320
|
+
const factor = assertNotCancelled(await text({
|
|
94321
|
+
message: "Task hours per unit of Story Estimate:",
|
|
94322
|
+
initialValue: current?.factor !== undefined ? String(current.factor) : "",
|
|
94323
|
+
validate: Validators.positiveNumber("Factor")
|
|
94324
|
+
}));
|
|
94325
|
+
return { factor: Number(factor) };
|
|
94326
|
+
}
|
|
94327
|
+
const table = await promptConversionTable(current?.table, sourceField);
|
|
94328
|
+
const keysAreCategories = !Object.keys(table).every((key) => /^(?:\d+(?:\.\d+)?|\.\d+)$/.test(key));
|
|
94329
|
+
const multipliers = keysAreCategories && assertNotCancelled(await confirm({
|
|
94330
|
+
message: "Also accept multiples of a size, e.g. 0.3XL = 0.3 × XL?",
|
|
94331
|
+
initialValue: current?.multipliers === true
|
|
94332
|
+
}));
|
|
94333
|
+
return { table, ...multipliers ? { multipliers: true } : {} };
|
|
94334
|
+
}
|
|
94335
|
+
function renderConversionTable(table, source) {
|
|
94336
|
+
const allowed = source?.allowedValues ?? [];
|
|
94337
|
+
const entries = Object.entries(table);
|
|
94338
|
+
const heading = `Conversion table — ${source?.name ?? "Story Estimate"} → hours`;
|
|
94339
|
+
const lines = [allowed.length ? `${heading} ${allowed.filter((v2) => Object.hasOwn(table, v2)).length} of ${allowed.length} values covered` : heading];
|
|
94340
|
+
if (entries.length === 0) {
|
|
94341
|
+
lines.push(" (no values yet)");
|
|
94342
|
+
} else {
|
|
94343
|
+
const width = Math.max(5, ...entries.map(([key]) => key.length));
|
|
94344
|
+
lines.push(` ${"Value".padEnd(width)} ${"Hours".padEnd(6)} A 20% task gets`);
|
|
94345
|
+
for (const [key, hours] of entries) {
|
|
94346
|
+
const flag = allowed.length && !allowed.includes(key) ? " not in picklist" : "";
|
|
94347
|
+
lines.push(` ${key.padEnd(width)} ${String(hours).padEnd(6)} ${Math.round(hours * 20) / 100} h${flag}`);
|
|
94348
|
+
}
|
|
94349
|
+
}
|
|
94350
|
+
const missing = allowed.filter((value) => !Object.hasOwn(table, value));
|
|
94351
|
+
if (missing.length)
|
|
94352
|
+
lines.push(` Missing: ${missing.join(", ")} — Stories with ${missing.length === 1 ? "this value" : "these values"} get blank estimates.`);
|
|
94353
|
+
return lines;
|
|
94354
|
+
}
|
|
94355
|
+
async function promptConversionTable(current, sourceField) {
|
|
94356
|
+
const allowed = sourceField?.isPicklist ? sourceField.allowedValues ?? [] : [];
|
|
94357
|
+
const table = { ...current ?? {} };
|
|
94358
|
+
if (allowed.length && !current) {
|
|
94359
|
+
output3.print(source_default.gray(`Enter the Task hours for each ${sourceField?.name ?? "value"}. Leave blank to skip a value.`));
|
|
94360
|
+
for (const [index, value] of allowed.entries()) {
|
|
94361
|
+
const hours = assertNotCancelled(await text({
|
|
94362
|
+
message: `Hours for "${value}" (${index + 1} of ${allowed.length}):`,
|
|
94363
|
+
validate: (input2) => input2 && input2.trim() !== "" ? Validators.nonNegative("Hours")(input2) : undefined
|
|
94364
|
+
}));
|
|
94365
|
+
if (hours.trim() !== "")
|
|
94366
|
+
table[value] = Number(hours);
|
|
94367
|
+
}
|
|
94368
|
+
}
|
|
94369
|
+
for (;; ) {
|
|
94370
|
+
output3.blankLine();
|
|
94371
|
+
for (const line of renderConversionTable(table, sourceField))
|
|
94372
|
+
output3.print(source_default.gray(line));
|
|
94373
|
+
const missing = allowed.filter((value) => !Object.hasOwn(table, value));
|
|
94374
|
+
const keys = Object.keys(table);
|
|
94375
|
+
const action = assertNotCancelled(await select({
|
|
94376
|
+
message: "Conversion table",
|
|
94377
|
+
options: [
|
|
94378
|
+
...missing.map((value) => ({ label: `Add hours for ${value}`, value: `add:${value}` })),
|
|
94379
|
+
{ label: allowed.length ? "Add another value…" : "Add a value", value: "add" },
|
|
94380
|
+
...keys.length ? [{ label: "Change a value's hours", value: "change" }, { label: "Remove a value", value: "remove" }] : [],
|
|
94381
|
+
{ label: "Paste several (e.g. S=2, M=4)", value: "paste" },
|
|
94382
|
+
{ label: "Done", value: "done" }
|
|
94383
|
+
],
|
|
94384
|
+
initialValue: missing.length ? `add:${missing[0]}` : keys.length ? "done" : "add"
|
|
94385
|
+
}));
|
|
94386
|
+
if (action === "done") {
|
|
94387
|
+
if (keys.length)
|
|
94388
|
+
return table;
|
|
94389
|
+
output3.print(source_default.yellow("Add at least one value before finishing."));
|
|
94390
|
+
continue;
|
|
94391
|
+
}
|
|
94392
|
+
if (action.startsWith("add:")) {
|
|
94393
|
+
const value = action.slice(4);
|
|
94394
|
+
table[value] = Number(assertNotCancelled(await text({ message: `Hours for "${value}":`, validate: hoursValidator })));
|
|
94395
|
+
continue;
|
|
94396
|
+
}
|
|
94397
|
+
if (action === "add") {
|
|
94398
|
+
const value = assertNotCancelled(await text({
|
|
94399
|
+
message: "Story Estimate value (e.g. XL, or 8 for points):",
|
|
94400
|
+
validate: (input2) => {
|
|
94401
|
+
if (!input2 || input2.trim() === "")
|
|
94402
|
+
return "Enter a value";
|
|
94403
|
+
if (Object.hasOwn(table, input2.trim()))
|
|
94404
|
+
return `"${input2.trim()}" is already in the table`;
|
|
94405
|
+
return;
|
|
94406
|
+
}
|
|
94407
|
+
})).trim();
|
|
94408
|
+
table[value] = Number(assertNotCancelled(await text({ message: `Hours for "${value}":`, validate: hoursValidator })));
|
|
94409
|
+
continue;
|
|
94410
|
+
}
|
|
94411
|
+
if (action === "change") {
|
|
94412
|
+
const value = assertNotCancelled(await select({ message: "Which value?", options: keys.map((key) => ({ label: `${key} (${table[key]} h)`, value: key })) }));
|
|
94413
|
+
table[value] = Number(assertNotCancelled(await text({ message: `Hours for "${value}":`, initialValue: String(table[value]), validate: hoursValidator })));
|
|
94414
|
+
continue;
|
|
94415
|
+
}
|
|
94416
|
+
if (action === "remove") {
|
|
94417
|
+
const value = assertNotCancelled(await select({ message: "Remove which value?", options: keys.map((key) => ({ label: `${key} (${table[key]} h)`, value: key })) }));
|
|
94418
|
+
delete table[value];
|
|
94419
|
+
continue;
|
|
94420
|
+
}
|
|
94421
|
+
const pasted = assertNotCancelled(await text({ message: "Value=hours pairs, comma-separated (e.g. S=2, M=4, L=5):", validate: Validators.conversionTable }));
|
|
94422
|
+
Object.assign(table, parseConversionTable(pasted));
|
|
94423
|
+
}
|
|
94424
|
+
}
|
|
94425
|
+
async function promptEstimateTarget(currentTaskType, currentTargetFields, ctx) {
|
|
94426
|
+
const customise = assertNotCancelled(await confirm({
|
|
94427
|
+
message: `Create tasks as a different work item type, or write estimates to different fields? (default: ${ctx.defaults?.taskWorkItemType ?? "platform default"})`,
|
|
94428
|
+
initialValue: currentTaskType !== undefined || (currentTargetFields?.length ?? 0) > 0
|
|
94429
|
+
}));
|
|
94430
|
+
if (!customise)
|
|
94431
|
+
return {};
|
|
94432
|
+
const defaultType = ctx.defaults?.taskWorkItemType;
|
|
94433
|
+
let taskType;
|
|
94434
|
+
if (ctx.workItemTypes?.length) {
|
|
94435
|
+
taskType = assertNotCancelled(await select({
|
|
94436
|
+
message: "Work item type to create tasks as:",
|
|
94437
|
+
options: ctx.workItemTypes.map((type) => ({ label: type, value: type, hint: type === defaultType ? "platform default" : undefined })),
|
|
94438
|
+
initialValue: currentTaskType ?? defaultType ?? ctx.workItemTypes[0]
|
|
94439
|
+
}));
|
|
94440
|
+
} else {
|
|
94441
|
+
taskType = assertNotCancelled(await text({
|
|
94442
|
+
message: "Work item type to create tasks as:",
|
|
94443
|
+
initialValue: currentTaskType ?? defaultType ?? "",
|
|
94444
|
+
validate: Validators.required("Work item type")
|
|
94445
|
+
})).trim();
|
|
94446
|
+
}
|
|
94447
|
+
const numericFields = ctx.getTaskFields ? sortEstimateFields((await ctx.getTaskFields(taskType)).filter((field) => isEstimateTargetField(field, ctx.defaults))) : [];
|
|
94448
|
+
let targetFields;
|
|
94449
|
+
if (numericFields.length > 0) {
|
|
94450
|
+
targetFields = assertNotCancelled(await multiselect({
|
|
94451
|
+
message: "Task fields that receive the estimate (none selected = platform default fields):",
|
|
94452
|
+
options: numericFields.map((field) => ({ label: `${field.name} (${field.referenceName})`, value: field.referenceName })),
|
|
94453
|
+
initialValues: currentTargetFields ?? [],
|
|
94454
|
+
required: false
|
|
94455
|
+
}));
|
|
94456
|
+
} else {
|
|
94457
|
+
const typed = assertNotCancelled(await text({
|
|
94458
|
+
message: "Task field reference names that receive the estimate, comma-separated (blank = platform default fields):",
|
|
94459
|
+
initialValue: currentTargetFields?.join(", ") ?? ""
|
|
94460
|
+
}));
|
|
94461
|
+
targetFields = Filters.commaSeparated(typed).filter(Boolean);
|
|
94462
|
+
}
|
|
94463
|
+
const keepTaskType = taskType !== defaultType || currentTaskType !== undefined;
|
|
94464
|
+
return { ...keepTaskType ? { taskType } : {}, ...targetFields.length ? { targetFields } : {} };
|
|
94465
|
+
}
|
|
94466
|
+
async function promptDefaultStoryEstimate(current, conversion, sourceValues) {
|
|
94467
|
+
const choices = (sourceValues.length ? sourceValues : Object.keys(conversion?.table ?? {})).filter((value) => isConvertibleStoryEstimate(value, conversion));
|
|
94468
|
+
if (choices.length > 0) {
|
|
94469
|
+
const currentValue = current === undefined ? undefined : String(current);
|
|
94470
|
+
const choice = assertNotCancelled(await select({
|
|
94471
|
+
message: "Default Story Estimate to assume:",
|
|
94472
|
+
options: [
|
|
94473
|
+
...choices.map((value) => ({ label: value, value })),
|
|
94474
|
+
...conversion?.multipliers ? [{ label: "Another value (e.g. 0.5L)", value: CUSTOM_ENTRY }] : []
|
|
94475
|
+
],
|
|
94476
|
+
initialValue: currentValue && choices.includes(currentValue) ? currentValue : currentValue && conversion?.multipliers ? CUSTOM_ENTRY : choices[0]
|
|
94477
|
+
}));
|
|
94478
|
+
if (choice !== CUSTOM_ENTRY)
|
|
94479
|
+
return choice;
|
|
94480
|
+
}
|
|
94481
|
+
const typed = assertNotCancelled(await text({
|
|
94482
|
+
message: "Default Story Estimate to assume (e.g. 5, or a size like M):",
|
|
94483
|
+
initialValue: current !== undefined ? String(current) : "",
|
|
94484
|
+
validate: (input2) => {
|
|
94485
|
+
if (!input2 || input2.trim() === "")
|
|
94486
|
+
return "A default Story Estimate is required";
|
|
94487
|
+
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";
|
|
94488
|
+
}
|
|
94489
|
+
})).trim();
|
|
94490
|
+
return Number.isFinite(Number(typed)) ? Number(typed) : typed;
|
|
93712
94491
|
}
|
|
93713
94492
|
async function configureValidation(defaults) {
|
|
93714
94493
|
const validation = {};
|
|
@@ -93953,9 +94732,15 @@ async function promptMixinRefs(mixins) {
|
|
|
93953
94732
|
function formatCatalogChoice(item) {
|
|
93954
94733
|
return `${item.displayName} (${item.scope})`;
|
|
93955
94734
|
}
|
|
93956
|
-
var output3, ADVANCED_GROUP_ORDER, DATE_FILTER_OPTIONS
|
|
94735
|
+
var output3, ADVANCED_GROUP_ORDER, DATE_FILTER_OPTIONS, CUSTOM_ENTRY = "__custom__", hoursValidator = (input2) => {
|
|
94736
|
+
if (!input2 || input2.trim() === "")
|
|
94737
|
+
return "Enter the hours";
|
|
94738
|
+
return Validators.nonNegative("Hours")(input2);
|
|
94739
|
+
};
|
|
93957
94740
|
var init_template_wizard_helper_command = __esm(() => {
|
|
93958
94741
|
init_dist6();
|
|
94742
|
+
init_estimation_grounding();
|
|
94743
|
+
init_schema2();
|
|
93959
94744
|
init_source();
|
|
93960
94745
|
init_command_output();
|
|
93961
94746
|
init_prompt_utilities();
|
|
@@ -97894,7 +98679,7 @@ var program = new Command;
|
|
|
97894
98679
|
|
|
97895
98680
|
// package.json
|
|
97896
98681
|
var name = "@sppg2001/atomize";
|
|
97897
|
-
var version2 = "4.0
|
|
98682
|
+
var version2 = "4.1.0";
|
|
97898
98683
|
|
|
97899
98684
|
// src/cli/commands/auth/auth-add.command.ts
|
|
97900
98685
|
init_dist6();
|
|
@@ -99396,16 +100181,26 @@ function printReport(report, options, dryRun, storyIds) {
|
|
|
99396
100181
|
`));
|
|
99397
100182
|
for (const result of report.results) {
|
|
99398
100183
|
if (result.success) {
|
|
100184
|
+
if (result.skipReason) {
|
|
100185
|
+
output2.print(source_default.yellow(`⊘ ${sanitizeTty(result.story.id)}: ${sanitizeTty(result.story.title)}`));
|
|
100186
|
+
output2.print(source_default.gray(` Skipped: ${sanitizeTty(result.skipReason)}`));
|
|
100187
|
+
continue;
|
|
100188
|
+
}
|
|
99399
100189
|
output2.print(source_default.green(`✓ ${sanitizeTty(result.story.id)}: ${sanitizeTty(result.story.title)}`));
|
|
99400
|
-
|
|
100190
|
+
const withUnit = (value) => result.estimateUnit ? `${value} ${result.estimateUnit}` : String(value);
|
|
100191
|
+
const rawEstimate = result.storyEstimate;
|
|
100192
|
+
const converted = result.estimationSummary?.storyEstimation;
|
|
100193
|
+
const conversionNote = converted !== undefined && converted !== rawEstimate ? ` (converted to ${withUnit(converted)})` : "";
|
|
100194
|
+
output2.print(source_default.gray(` Estimation: ${rawEstimate === undefined ? "none" : sanitizeTty(String(rawEstimate))}${conversionNote}`));
|
|
99401
100195
|
output2.print(source_default.gray(` Tasks: ${result.tasksCalculated.length}`));
|
|
99402
100196
|
if (result.estimationSummary) {
|
|
99403
|
-
output2.print(source_default.gray(` Distribution: ${result.estimationSummary.totalTaskEstimation}
|
|
100197
|
+
output2.print(source_default.gray(` Distribution: ${withUnit(result.estimationSummary.totalTaskEstimation)} (${result.estimationSummary.percentageUsed.toFixed(0)}%)`));
|
|
99404
100198
|
}
|
|
99405
100199
|
if ((options.verbose || dryRun) && result.tasksCalculated.length > 0) {
|
|
99406
100200
|
output2.print(source_default.gray(" Task breakdown:"));
|
|
99407
100201
|
for (const task of result.tasksCalculated) {
|
|
99408
|
-
|
|
100202
|
+
const estimate = task.estimation === undefined ? "unestimated" : withUnit(task.estimation);
|
|
100203
|
+
output2.print(source_default.gray(` - ${sanitizeTty(task.title)}: ${estimate} (${task.estimationPercent}%)`));
|
|
99409
100204
|
}
|
|
99410
100205
|
}
|
|
99411
100206
|
} else {
|
|
@@ -99577,6 +100372,8 @@ function makeGenerateCommand(makeOutput, prompts, config2) {
|
|
|
99577
100372
|
});
|
|
99578
100373
|
}
|
|
99579
100374
|
async function resolveNormalization(template, isTTYSession, prompts) {
|
|
100375
|
+
if (template.estimation?.normalize && template.estimation.normalize !== "auto")
|
|
100376
|
+
return false;
|
|
99580
100377
|
const { shouldOffer, total } = shouldOfferOverageNormalization(template.tasks);
|
|
99581
100378
|
if (!shouldOffer)
|
|
99582
100379
|
return false;
|
|
@@ -99594,6 +100391,7 @@ async function resolveNormalization(template, isTTYSession, prompts) {
|
|
|
99594
100391
|
init_condition_evaluator();
|
|
99595
100392
|
init_dependency_resolver();
|
|
99596
100393
|
init_estimation_calculator();
|
|
100394
|
+
init_mock_adapter();
|
|
99597
100395
|
var STANDARD_FIELD_TYPES = {
|
|
99598
100396
|
id: "string",
|
|
99599
100397
|
title: "string",
|
|
@@ -99652,7 +100450,11 @@ function inspectTemplate(template) {
|
|
|
99652
100450
|
}
|
|
99653
100451
|
}
|
|
99654
100452
|
if (template.tasks.some((task) => task.estimationPercent !== undefined || task.estimationPercentCondition?.length)) {
|
|
99655
|
-
|
|
100453
|
+
const source = template.estimation?.source;
|
|
100454
|
+
if (source)
|
|
100455
|
+
upsert(source, "unknown", "estimation", { required: true });
|
|
100456
|
+
else
|
|
100457
|
+
upsert("estimation", "number", "estimation", { required: true });
|
|
99656
100458
|
}
|
|
99657
100459
|
for (const name2 of filterFields) {
|
|
99658
100460
|
upsert(name2, STANDARD_FIELD_TYPES[name2] ?? "unknown", "filter");
|
|
@@ -99695,13 +100497,16 @@ function runPreview(template, mockStoryJson) {
|
|
|
99695
100497
|
const story = parseMockStory(mockStoryJson);
|
|
99696
100498
|
const orderedTasks = new DependencyResolver().resolveDependencies(template.tasks);
|
|
99697
100499
|
const calc = new EstimationCalculator;
|
|
99698
|
-
const
|
|
99699
|
-
|
|
100500
|
+
const fieldMapping = resolveEstimationFieldMapping(MOCK_ESTIMATION_DEFAULTS, {
|
|
100501
|
+
source: template.estimation?.source,
|
|
100502
|
+
targetFields: template.estimation?.targetFields
|
|
100503
|
+
});
|
|
100504
|
+
const { calculatedTasks, skippedTasks, unresolvedEstimate } = calc.calculateTasksWithSkipped(story, "", orderedTasks, template.estimation, false, fieldMapping);
|
|
100505
|
+
const summary = calc.getEstimationSummary(story, calculatedTasks, template.estimation, fieldMapping);
|
|
99700
100506
|
const tasks = calculatedTasks.map((task) => {
|
|
99701
|
-
const result = {
|
|
99702
|
-
|
|
99703
|
-
estimation
|
|
99704
|
-
};
|
|
100507
|
+
const result = { title: task.title };
|
|
100508
|
+
if (task.estimation !== undefined)
|
|
100509
|
+
result.estimation = task.estimation;
|
|
99705
100510
|
if (task.estimationPercent !== undefined)
|
|
99706
100511
|
result.estimationPercent = task.estimationPercent;
|
|
99707
100512
|
if (task.dependsOn?.length)
|
|
@@ -99720,6 +100525,8 @@ function runPreview(template, mockStoryJson) {
|
|
|
99720
100525
|
title: s2.templateTask.title,
|
|
99721
100526
|
reason: s2.reason
|
|
99722
100527
|
})),
|
|
100528
|
+
...unresolvedEstimate ? { unresolvedEstimate } : {},
|
|
100529
|
+
...fieldMapping.unitLabel ? { estimateUnit: fieldMapping.unitLabel } : {},
|
|
99723
100530
|
estimationSummary: {
|
|
99724
100531
|
storyEstimation: summary.storyEstimation,
|
|
99725
100532
|
totalTaskEstimation: summary.totalTaskEstimation,
|
|
@@ -100041,6 +100848,70 @@ init_errors();
|
|
|
100041
100848
|
// ../atomize-core/src/services/template/learned-template-product.ts
|
|
100042
100849
|
init_estimation_distribution();
|
|
100043
100850
|
|
|
100851
|
+
// ../atomize-core/src/core/estimation-inference.ts
|
|
100852
|
+
function taskEstimateShares(taskEstimates) {
|
|
100853
|
+
const total = taskEstimates.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
100854
|
+
if (total <= 0)
|
|
100855
|
+
return taskEstimates.map(() => 0);
|
|
100856
|
+
return taskEstimates.map((value) => Math.round((value ?? 0) / total * 100));
|
|
100857
|
+
}
|
|
100858
|
+
function inferEstimation(samples) {
|
|
100859
|
+
const usable = samples.map((sample) => ({ estimate: sample.storyEstimate, total: sum(sample.taskEstimates) })).filter((s2) => s2.estimate !== undefined && s2.total > 0);
|
|
100860
|
+
if (usable.length === 0)
|
|
100861
|
+
return { lowConfidenceValues: [] };
|
|
100862
|
+
const defaultStoryEstimate = mostCommon(usable.map((s2) => s2.estimate));
|
|
100863
|
+
const allNumeric = usable.every((s2) => numericStoryEstimate(s2.estimate) !== undefined);
|
|
100864
|
+
if (allNumeric) {
|
|
100865
|
+
const ratios = usable.map((s2) => ({ points: numericStoryEstimate(s2.estimate) ?? 0, total: s2.total })).filter((s2) => s2.points > 0).map((s2) => s2.total / s2.points);
|
|
100866
|
+
if (ratios.length === 0)
|
|
100867
|
+
return { defaultStoryEstimate, lowConfidenceValues: [] };
|
|
100868
|
+
const factor = roundTo(median(ratios), 2);
|
|
100869
|
+
return {
|
|
100870
|
+
...factor !== 1 ? { conversion: { factor } } : {},
|
|
100871
|
+
defaultStoryEstimate,
|
|
100872
|
+
lowConfidenceValues: []
|
|
100873
|
+
};
|
|
100874
|
+
}
|
|
100875
|
+
const totalsByValue = new Map;
|
|
100876
|
+
for (const s2 of usable) {
|
|
100877
|
+
const key = String(s2.estimate).trim();
|
|
100878
|
+
totalsByValue.set(key, [...totalsByValue.get(key) ?? [], s2.total]);
|
|
100879
|
+
}
|
|
100880
|
+
const table = {};
|
|
100881
|
+
const lowConfidenceValues = [];
|
|
100882
|
+
for (const [key, totals] of totalsByValue) {
|
|
100883
|
+
table[key] = roundTo(median(totals), 2);
|
|
100884
|
+
if (totals.length === 1)
|
|
100885
|
+
lowConfidenceValues.push(key);
|
|
100886
|
+
}
|
|
100887
|
+
return { conversion: { table }, defaultStoryEstimate, lowConfidenceValues };
|
|
100888
|
+
}
|
|
100889
|
+
function sum(values) {
|
|
100890
|
+
return values.reduce((total, value) => total + (value ?? 0), 0);
|
|
100891
|
+
}
|
|
100892
|
+
function median(values) {
|
|
100893
|
+
const sorted = [...values].sort((a3, b3) => a3 - b3);
|
|
100894
|
+
const mid = Math.floor(sorted.length / 2);
|
|
100895
|
+
return sorted.length % 2 === 0 ? ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2 : sorted[mid] ?? 0;
|
|
100896
|
+
}
|
|
100897
|
+
function mostCommon(values) {
|
|
100898
|
+
const counts = new Map;
|
|
100899
|
+
for (const value of values)
|
|
100900
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
100901
|
+
let best;
|
|
100902
|
+
let bestCount = 0;
|
|
100903
|
+
for (const [value, count] of counts) {
|
|
100904
|
+
if (count > bestCount) {
|
|
100905
|
+
best = value;
|
|
100906
|
+
bestCount = count;
|
|
100907
|
+
}
|
|
100908
|
+
}
|
|
100909
|
+
return best;
|
|
100910
|
+
}
|
|
100911
|
+
function roundTo(value, decimals) {
|
|
100912
|
+
const scale = 10 ** decimals;
|
|
100913
|
+
return Math.round(value * scale) / scale;
|
|
100914
|
+
}
|
|
100044
100915
|
// ../atomize-core/src/services/template/pattern-detection.ts
|
|
100045
100916
|
init_dist();
|
|
100046
100917
|
var DEFAULT_PATTERN_SCORING_CONFIG = {
|
|
@@ -100631,12 +101502,12 @@ class ConditionPatternDetector {
|
|
|
100631
101502
|
}
|
|
100632
101503
|
detectEstimationConditions(analyses, commonTasks, threshold) {
|
|
100633
101504
|
const patterns = [];
|
|
100634
|
-
const estimations = analyses.map((a3) => a3.story.estimation).filter((e2) => e2 !== undefined && e2 > 0).sort((a3, b3) => a3 - b3);
|
|
101505
|
+
const estimations = analyses.map((a3) => numericStoryEstimate(a3.story.estimation)).filter((e2) => e2 !== undefined && e2 > 0).sort((a3, b3) => a3 - b3);
|
|
100635
101506
|
if (estimations.length < this.minSampleSize)
|
|
100636
101507
|
return patterns;
|
|
100637
|
-
const
|
|
100638
|
-
const q75 = estimations[Math.floor(estimations.length * 3 / 4)] ??
|
|
100639
|
-
const thresholds = [...new Set([
|
|
101508
|
+
const median2 = estimations[Math.floor(estimations.length / 2)] ?? 0;
|
|
101509
|
+
const q75 = estimations[Math.floor(estimations.length * 3 / 4)] ?? median2;
|
|
101510
|
+
const thresholds = [...new Set([median2, q75])].filter((t3) => t3 > 0);
|
|
100640
101511
|
for (const commonTask of commonTasks) {
|
|
100641
101512
|
if (commonTask.frequencyRatio >= 0.9)
|
|
100642
101513
|
continue;
|
|
@@ -100666,7 +101537,7 @@ class ConditionPatternDetector {
|
|
|
100666
101537
|
let taskInLargeStories = 0;
|
|
100667
101538
|
let taskInSmallStories = 0;
|
|
100668
101539
|
for (const analysis of analyses) {
|
|
100669
|
-
const estimation = analysis.story.estimation;
|
|
101540
|
+
const estimation = numericStoryEstimate(analysis.story.estimation);
|
|
100670
101541
|
if (estimation === undefined)
|
|
100671
101542
|
continue;
|
|
100672
101543
|
const isLarge = estimation >= estimationThreshold;
|
|
@@ -100779,7 +101650,7 @@ class ConditionPatternDetector {
|
|
|
100779
101650
|
return false;
|
|
100780
101651
|
}
|
|
100781
101652
|
case "estimation": {
|
|
100782
|
-
const e2 = story.estimation;
|
|
101653
|
+
const e2 = numericStoryEstimate(story.estimation);
|
|
100783
101654
|
if (e2 === undefined)
|
|
100784
101655
|
return false;
|
|
100785
101656
|
const v2 = condition.value;
|
|
@@ -100950,18 +101821,18 @@ class FilterLearner {
|
|
|
100950
101821
|
for (const p3 of priorities) {
|
|
100951
101822
|
priorityCounts.set(p3, (priorityCounts.get(p3) ?? 0) + 1);
|
|
100952
101823
|
}
|
|
100953
|
-
let
|
|
101824
|
+
let mostCommon2 = min;
|
|
100954
101825
|
let maxCount = 0;
|
|
100955
101826
|
for (const [priority, count] of priorityCounts) {
|
|
100956
101827
|
if (count > maxCount) {
|
|
100957
|
-
|
|
101828
|
+
mostCommon2 = priority;
|
|
100958
101829
|
maxCount = count;
|
|
100959
101830
|
}
|
|
100960
101831
|
}
|
|
100961
|
-
return { min, max, mostCommon };
|
|
101832
|
+
return { min, max, mostCommon: mostCommon2 };
|
|
100962
101833
|
}
|
|
100963
101834
|
detectEstimationRange(analyses) {
|
|
100964
|
-
const estimations = analyses.map((a3) => a3.story.estimation).filter((e2) => e2 !== undefined && e2 > 0);
|
|
101835
|
+
const estimations = analyses.map((a3) => numericStoryEstimate(a3.story.estimation)).filter((e2) => e2 !== undefined && e2 > 0);
|
|
100965
101836
|
if (estimations.length === 0)
|
|
100966
101837
|
return;
|
|
100967
101838
|
const sorted = [...estimations].sort((a3, b3) => a3 - b3);
|
|
@@ -101042,7 +101913,7 @@ class FilterLearner {
|
|
|
101042
101913
|
if (max - min <= 3) {
|
|
101043
101914
|
suggestions.push({
|
|
101044
101915
|
type: "improve-filter",
|
|
101045
|
-
message: `Stories have similar estimations (${min}-${max}
|
|
101916
|
+
message: `Stories have similar estimations (${min}-${max}, avg ${average}). This template works well for stories of this size.`,
|
|
101046
101917
|
severity: "info"
|
|
101047
101918
|
});
|
|
101048
101919
|
}
|
|
@@ -101065,8 +101936,8 @@ class FilterLearner {
|
|
|
101065
101936
|
parts.push(`Area paths: ${learnedFilters.areaPaths.values.join(", ")}`);
|
|
101066
101937
|
}
|
|
101067
101938
|
if (learnedFilters.priorityRange) {
|
|
101068
|
-
const { min, max, mostCommon } = learnedFilters.priorityRange;
|
|
101069
|
-
parts.push(`Priority range: ${min}-${max} (most common: ${
|
|
101939
|
+
const { min, max, mostCommon: mostCommon2 } = learnedFilters.priorityRange;
|
|
101940
|
+
parts.push(`Priority range: ${min}-${max} (most common: ${mostCommon2})`);
|
|
101070
101941
|
}
|
|
101071
101942
|
if (learnedFilters.estimationRange) {
|
|
101072
101943
|
const { min, max, average } = learnedFilters.estimationRange;
|
|
@@ -101215,7 +102086,7 @@ class PatternDetector extends SimilarityCalculator {
|
|
|
101215
102086
|
detectEstimationPattern(analyses) {
|
|
101216
102087
|
const totalEstimations = [];
|
|
101217
102088
|
for (const analysis of analyses) {
|
|
101218
|
-
const storyEstimation = analysis.story.estimation ?? 0;
|
|
102089
|
+
const storyEstimation = numericStoryEstimate(analysis.story.estimation) ?? 0;
|
|
101219
102090
|
totalEstimations.push(storyEstimation);
|
|
101220
102091
|
}
|
|
101221
102092
|
const avgTotal = totalEstimations.length > 0 ? totalEstimations.reduce((a3, b3) => a3 + b3, 0) / totalEstimations.length : 0;
|
|
@@ -101297,7 +102168,7 @@ class ConfidenceScorer {
|
|
|
101297
102168
|
};
|
|
101298
102169
|
}
|
|
101299
102170
|
scoreTaskConsistency(analyses, patterns) {
|
|
101300
|
-
const totalTasks = analyses.reduce((
|
|
102171
|
+
const totalTasks = analyses.reduce((sum2, a3) => sum2 + a3.tasks.length, 0);
|
|
101301
102172
|
if (totalTasks === 0) {
|
|
101302
102173
|
return {
|
|
101303
102174
|
name: "Task Consistency",
|
|
@@ -101377,9 +102248,9 @@ class ConfidenceScorer {
|
|
|
101377
102248
|
description: "No tasks to merge"
|
|
101378
102249
|
};
|
|
101379
102250
|
}
|
|
101380
|
-
const totalOriginalTasks = analyses.reduce((
|
|
102251
|
+
const totalOriginalTasks = analyses.reduce((sum2, a3) => sum2 + a3.tasks.length, 0);
|
|
101381
102252
|
const mergeRatio = totalOriginalTasks > 0 ? 1 - mergedTasks.length / totalOriginalTasks : 0;
|
|
101382
|
-
const avgSimilarity = mergedTasks.reduce((
|
|
102253
|
+
const avgSimilarity = mergedTasks.reduce((sum2, m3) => sum2 + m3.similarity, 0) / mergedTasks.length;
|
|
101383
102254
|
const compositeScore = mergeRatio * 0.6 + avgSimilarity * 0.4;
|
|
101384
102255
|
const score = Math.round(compositeScore * 100);
|
|
101385
102256
|
return {
|
|
@@ -101418,7 +102289,7 @@ class ConfidenceScorer {
|
|
|
101418
102289
|
description: "No dependency patterns detected"
|
|
101419
102290
|
};
|
|
101420
102291
|
}
|
|
101421
|
-
const avgConfidence = dependencyPatterns.reduce((
|
|
102292
|
+
const avgConfidence = dependencyPatterns.reduce((sum2, d2) => sum2 + d2.confidence, 0) / dependencyPatterns.length;
|
|
101422
102293
|
const explicitCount = dependencyPatterns.filter((d2) => d2.source === "explicit").length;
|
|
101423
102294
|
const explicitRatio = explicitCount / dependencyPatterns.length;
|
|
101424
102295
|
const explicitBonus = explicitRatio * 10;
|
|
@@ -101440,8 +102311,8 @@ class ConfidenceScorer {
|
|
|
101440
102311
|
description: "No conditional patterns detected"
|
|
101441
102312
|
};
|
|
101442
102313
|
}
|
|
101443
|
-
const avgConfidence = conditionalPatterns.reduce((
|
|
101444
|
-
const avgCoverage = conditionalPatterns.reduce((
|
|
102314
|
+
const avgConfidence = conditionalPatterns.reduce((sum2, c4) => sum2 + c4.confidence, 0) / conditionalPatterns.length;
|
|
102315
|
+
const avgCoverage = conditionalPatterns.reduce((sum2, c4) => sum2 + (c4.totalStories > 0 ? c4.matchCount / c4.totalStories : 0), 0) / conditionalPatterns.length;
|
|
101445
102316
|
const score = Math.round(Math.min(100, avgConfidence * 70 + avgCoverage * 30));
|
|
101446
102317
|
return {
|
|
101447
102318
|
name: "Condition Quality",
|
|
@@ -101451,7 +102322,7 @@ class ConfidenceScorer {
|
|
|
101451
102322
|
};
|
|
101452
102323
|
}
|
|
101453
102324
|
calculateOverall(factors, sampleCount) {
|
|
101454
|
-
const baseScore = factors.reduce((
|
|
102325
|
+
const baseScore = factors.reduce((sum2, f2) => sum2 + f2.score * f2.weight, 0);
|
|
101455
102326
|
const extraWeightMap = {
|
|
101456
102327
|
1: 0.5,
|
|
101457
102328
|
2: 0.25,
|
|
@@ -101505,35 +102376,35 @@ class OutlierDetector {
|
|
|
101505
102376
|
const sorted = [...values].sort((a3, b3) => a3 - b3);
|
|
101506
102377
|
const n4 = sorted.length;
|
|
101507
102378
|
const midIndex = Math.floor(n4 / 2);
|
|
101508
|
-
const
|
|
101509
|
-
const deviations = values.map((v2) => Math.abs(v2 -
|
|
102379
|
+
const median2 = n4 % 2 === 1 ? sorted[midIndex] ?? 0 : ((sorted[midIndex - 1] ?? 0) + (sorted[midIndex] ?? 0)) / 2;
|
|
102380
|
+
const deviations = values.map((v2) => Math.abs(v2 - median2));
|
|
101510
102381
|
const sortedDev = [...deviations].sort((a3, b3) => a3 - b3);
|
|
101511
102382
|
const mad = n4 % 2 === 1 ? sortedDev[midIndex] ?? 0 : ((sortedDev[midIndex - 1] ?? 0) + (sortedDev[midIndex] ?? 0)) / 2;
|
|
101512
|
-
return { median, mad };
|
|
102383
|
+
return { median: median2, mad };
|
|
101513
102384
|
}
|
|
101514
|
-
modifiedZScore(value,
|
|
102385
|
+
modifiedZScore(value, median2, mad) {
|
|
101515
102386
|
if (mad === 0)
|
|
101516
102387
|
return 0;
|
|
101517
|
-
return this.madToStdDev * (value -
|
|
102388
|
+
return this.madToStdDev * (value - median2) / mad;
|
|
101518
102389
|
}
|
|
101519
102390
|
detectEstimationOutliers(analyses) {
|
|
101520
102391
|
const estimations = analyses.map((a3) => ({
|
|
101521
102392
|
storyId: a3.story.id,
|
|
101522
|
-
estimation: a3.story.estimation ?? 0
|
|
102393
|
+
estimation: numericStoryEstimate(a3.story.estimation) ?? 0
|
|
101523
102394
|
})).filter((e2) => e2.estimation > 0);
|
|
101524
102395
|
if (estimations.length < 2)
|
|
101525
102396
|
return [];
|
|
101526
102397
|
const values = estimations.map((e2) => e2.estimation);
|
|
101527
|
-
const { median, mad } = this.calculateMAD(values);
|
|
102398
|
+
const { median: median2, mad } = this.calculateMAD(values);
|
|
101528
102399
|
if (mad === 0)
|
|
101529
102400
|
return [];
|
|
101530
102401
|
const outliers = [];
|
|
101531
102402
|
for (const e2 of estimations) {
|
|
101532
|
-
const zScore = this.modifiedZScore(e2.estimation,
|
|
102403
|
+
const zScore = this.modifiedZScore(e2.estimation, median2, mad);
|
|
101533
102404
|
const absZ = Math.abs(zScore);
|
|
101534
102405
|
if (absZ > this.zThreshold) {
|
|
101535
|
-
const expectedLower =
|
|
101536
|
-
const expectedUpper =
|
|
102406
|
+
const expectedLower = median2 - this.zThreshold * mad / this.madToStdDev;
|
|
102407
|
+
const expectedUpper = median2 + this.zThreshold * mad / this.madToStdDev;
|
|
101537
102408
|
outliers.push({
|
|
101538
102409
|
type: "estimation",
|
|
101539
102410
|
storyId: e2.storyId,
|
|
@@ -101557,16 +102428,16 @@ class OutlierDetector {
|
|
|
101557
102428
|
if (counts.length < 2)
|
|
101558
102429
|
return [];
|
|
101559
102430
|
const values = counts.map((c4) => c4.count);
|
|
101560
|
-
const { median, mad } = this.calculateMAD(values);
|
|
102431
|
+
const { median: median2, mad } = this.calculateMAD(values);
|
|
101561
102432
|
if (mad === 0)
|
|
101562
102433
|
return [];
|
|
101563
102434
|
const outliers = [];
|
|
101564
102435
|
for (const c4 of counts) {
|
|
101565
|
-
const zScore = this.modifiedZScore(c4.count,
|
|
102436
|
+
const zScore = this.modifiedZScore(c4.count, median2, mad);
|
|
101566
102437
|
const absZ = Math.abs(zScore);
|
|
101567
102438
|
if (absZ > this.zThreshold) {
|
|
101568
|
-
const expectedLower =
|
|
101569
|
-
const expectedUpper =
|
|
102439
|
+
const expectedLower = median2 - this.zThreshold * mad / this.madToStdDev;
|
|
102440
|
+
const expectedUpper = median2 + this.zThreshold * mad / this.madToStdDev;
|
|
101570
102441
|
outliers.push({
|
|
101571
102442
|
type: "task-count",
|
|
101572
102443
|
storyId: c4.storyId,
|
|
@@ -101626,7 +102497,7 @@ class OutlierDetector {
|
|
|
101626
102497
|
|
|
101627
102498
|
// ../atomize-core/src/services/template/learned-template-product.ts
|
|
101628
102499
|
class LearnedTemplateProductBuilder {
|
|
101629
|
-
buildMergedTemplate(analyses, mergedTasks) {
|
|
102500
|
+
buildMergedTemplate(analyses, mergedTasks, estimationSource) {
|
|
101630
102501
|
const taskDefinitions = mergedTasks.map((mt) => {
|
|
101631
102502
|
const task = { ...mt.task };
|
|
101632
102503
|
if (mt.learnedDependsOn && mt.learnedDependsOn.length > 0) {
|
|
@@ -101647,8 +102518,6 @@ class LearnedTemplateProductBuilder {
|
|
|
101647
102518
|
normalizeLearnedTaskPercentages(taskDefinitions);
|
|
101648
102519
|
const workItemTypes = [...new Set(analyses.map((a3) => a3.story.type))];
|
|
101649
102520
|
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
102521
|
const storyIds = analyses.map((a3) => a3.story.id).join(", ");
|
|
101653
102522
|
return {
|
|
101654
102523
|
version: "1.0",
|
|
@@ -101662,12 +102531,7 @@ class LearnedTemplateProductBuilder {
|
|
|
101662
102531
|
tags: allTags.length > 0 ? { include: allTags } : undefined
|
|
101663
102532
|
},
|
|
101664
102533
|
tasks: taskDefinitions,
|
|
101665
|
-
estimation:
|
|
101666
|
-
strategy: "percentage",
|
|
101667
|
-
rounding: "none",
|
|
101668
|
-
minimumTaskPoints: 0,
|
|
101669
|
-
defaultParentEstimation: avgEstimation
|
|
101670
|
-
}
|
|
102534
|
+
estimation: learnedEstimationConfig(analyses.map((a3) => estimationSample(a3.story, a3.tasks)), estimationSource)
|
|
101671
102535
|
};
|
|
101672
102536
|
}
|
|
101673
102537
|
buildEmptyTemplate(story) {
|
|
@@ -101688,11 +102552,10 @@ class LearnedTemplateProductBuilder {
|
|
|
101688
102552
|
}
|
|
101689
102553
|
};
|
|
101690
102554
|
}
|
|
101691
|
-
buildSingleStoryTemplate(story, tasks) {
|
|
101692
|
-
const
|
|
102555
|
+
buildSingleStoryTemplate(story, tasks, estimationSource) {
|
|
102556
|
+
const shares = taskEstimateShares(tasks.map((task) => numericStoryEstimate(task.estimation)));
|
|
101693
102557
|
const taskDefinitions = tasks.map((task, index) => {
|
|
101694
|
-
const
|
|
101695
|
-
const estimationPercent = storyEstimation > 0 ? Math.round(taskEstimation / storyEstimation * 100) : 0;
|
|
102558
|
+
const estimationPercent = shares[index] ?? 0;
|
|
101696
102559
|
return {
|
|
101697
102560
|
id: this.generateTaskId(task.title, index),
|
|
101698
102561
|
title: this.extractTitlePattern(task.title, story.title),
|
|
@@ -101716,16 +102579,12 @@ class LearnedTemplateProductBuilder {
|
|
|
101716
102579
|
tags: story.tags?.length ? { include: story.tags } : undefined
|
|
101717
102580
|
},
|
|
101718
102581
|
tasks: taskDefinitions,
|
|
101719
|
-
estimation:
|
|
101720
|
-
strategy: "percentage",
|
|
101721
|
-
rounding: "none",
|
|
101722
|
-
minimumTaskPoints: 0,
|
|
101723
|
-
defaultParentEstimation: storyEstimation
|
|
101724
|
-
}
|
|
102582
|
+
estimation: learnedEstimationConfig([estimationSample(story, tasks)], estimationSource)
|
|
101725
102583
|
};
|
|
101726
102584
|
}
|
|
101727
102585
|
generateSuggestions(analyses, patterns, confidence, outliers) {
|
|
101728
102586
|
return [
|
|
102587
|
+
...this.suggestEstimationChecks(analyses),
|
|
101729
102588
|
...this.suggestConfidenceImprovements(analyses, confidence),
|
|
101730
102589
|
...this.suggestOutlierRemovals(outliers),
|
|
101731
102590
|
...this.suggestNamingImprovements(patterns),
|
|
@@ -101734,7 +102593,18 @@ class LearnedTemplateProductBuilder {
|
|
|
101734
102593
|
...this.suggestFilterImprovements(patterns)
|
|
101735
102594
|
];
|
|
101736
102595
|
}
|
|
101737
|
-
|
|
102596
|
+
suggestEstimationChecks(analyses) {
|
|
102597
|
+
const learned = inferEstimation(analyses.map((a3) => estimationSample(a3.story, a3.tasks)));
|
|
102598
|
+
const table = learned.conversion?.table;
|
|
102599
|
+
if (!table)
|
|
102600
|
+
return [];
|
|
102601
|
+
return learned.lowConfidenceValues.map((value) => ({
|
|
102602
|
+
type: "adjust-estimation",
|
|
102603
|
+
message: `The conversion for "${value}" (${table[value]}) was learned from a single Story; check it before relying on it.`,
|
|
102604
|
+
severity: "info"
|
|
102605
|
+
}));
|
|
102606
|
+
}
|
|
102607
|
+
generateVariations(analyses, patterns, mergedTasks, estimationSource) {
|
|
101738
102608
|
const confidenceScorer = new ConfidenceScorer;
|
|
101739
102609
|
const variations = [];
|
|
101740
102610
|
const coreTasks = mergedTasks.filter((mt) => {
|
|
@@ -101742,7 +102612,7 @@ class LearnedTemplateProductBuilder {
|
|
|
101742
102612
|
return storyCount / analyses.length >= 0.6;
|
|
101743
102613
|
});
|
|
101744
102614
|
if (coreTasks.length > 0 && coreTasks.length !== mergedTasks.length) {
|
|
101745
|
-
const coreTemplate = this.buildMergedTemplate(analyses, coreTasks);
|
|
102615
|
+
const coreTemplate = this.buildMergedTemplate(analyses, coreTasks, estimationSource);
|
|
101746
102616
|
coreTemplate.name = "Core Tasks Template";
|
|
101747
102617
|
coreTemplate.description = "Tasks appearing in 60%+ of analyzed stories";
|
|
101748
102618
|
variations.push({
|
|
@@ -101753,7 +102623,7 @@ class LearnedTemplateProductBuilder {
|
|
|
101753
102623
|
});
|
|
101754
102624
|
}
|
|
101755
102625
|
if (mergedTasks.length > 0) {
|
|
101756
|
-
const fullTemplate = this.buildMergedTemplate(analyses, mergedTasks);
|
|
102626
|
+
const fullTemplate = this.buildMergedTemplate(analyses, mergedTasks, estimationSource);
|
|
101757
102627
|
fullTemplate.name = "Comprehensive Template";
|
|
101758
102628
|
fullTemplate.description = "All tasks found across analyzed stories";
|
|
101759
102629
|
variations.push({
|
|
@@ -101876,6 +102746,23 @@ class LearnedTemplateProductBuilder {
|
|
|
101876
102746
|
return suggestions;
|
|
101877
102747
|
}
|
|
101878
102748
|
}
|
|
102749
|
+
function estimationSample(story, tasks) {
|
|
102750
|
+
return {
|
|
102751
|
+
storyEstimate: story.estimation,
|
|
102752
|
+
taskEstimates: tasks.map((task) => numericStoryEstimate(task.estimation))
|
|
102753
|
+
};
|
|
102754
|
+
}
|
|
102755
|
+
function learnedEstimationConfig(samples, source) {
|
|
102756
|
+
const learned = inferEstimation(samples);
|
|
102757
|
+
return {
|
|
102758
|
+
strategy: "percentage",
|
|
102759
|
+
rounding: "none",
|
|
102760
|
+
minimumTaskEstimate: 0,
|
|
102761
|
+
...source ? { source } : {},
|
|
102762
|
+
...learned.conversion ? { conversion: learned.conversion } : {},
|
|
102763
|
+
...learned.defaultStoryEstimate !== undefined ? { defaultParentEstimation: learned.defaultStoryEstimate } : {}
|
|
102764
|
+
};
|
|
102765
|
+
}
|
|
101879
102766
|
|
|
101880
102767
|
// ../atomize-core/src/services/template/task-merger.ts
|
|
101881
102768
|
class TaskMerger {
|
|
@@ -101969,15 +102856,15 @@ class TaskMerger {
|
|
|
101969
102856
|
for (const value of values) {
|
|
101970
102857
|
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
101971
102858
|
}
|
|
101972
|
-
let
|
|
102859
|
+
let mostCommon2 = values[0] ?? 0;
|
|
101973
102860
|
let maxCount = 0;
|
|
101974
102861
|
for (const [value, count] of counts) {
|
|
101975
|
-
if (count > maxCount || count === maxCount && value >
|
|
101976
|
-
|
|
102862
|
+
if (count > maxCount || count === maxCount && value > mostCommon2) {
|
|
102863
|
+
mostCommon2 = value;
|
|
101977
102864
|
maxCount = count;
|
|
101978
102865
|
}
|
|
101979
102866
|
}
|
|
101980
|
-
return
|
|
102867
|
+
return mostCommon2;
|
|
101981
102868
|
}
|
|
101982
102869
|
averagePriority(values) {
|
|
101983
102870
|
if (values.length === 0)
|
|
@@ -102082,27 +102969,28 @@ class StoryLearner {
|
|
|
102082
102969
|
this.learningSession = learningSession;
|
|
102083
102970
|
this.productBuilder = productBuilder;
|
|
102084
102971
|
}
|
|
102085
|
-
async learnFromStory(storyId) {
|
|
102972
|
+
async learnFromStory(storyId, options = {}) {
|
|
102086
102973
|
logger.info(`Learning template from story: ${storyId}`);
|
|
102087
|
-
const
|
|
102088
|
-
if (!
|
|
102974
|
+
const fetched = await this.platform.getWorkItem(storyId);
|
|
102975
|
+
if (!fetched) {
|
|
102089
102976
|
throw new TemplateGenerationError(`Story ${storyId} not found`);
|
|
102090
102977
|
}
|
|
102978
|
+
const story = withEstimationSource(fetched, options.estimationSource);
|
|
102091
102979
|
const tasks = await this.platform.getChildren(storyId);
|
|
102092
102980
|
if (!tasks || tasks.length === 0) {
|
|
102093
102981
|
throw new TemplateGenerationError(`Story ${storyId} has no child tasks to learn from`);
|
|
102094
102982
|
}
|
|
102095
102983
|
logger.info(`Found ${tasks.length} tasks to analyze`);
|
|
102096
|
-
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks);
|
|
102984
|
+
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks, options.estimationSource);
|
|
102097
102985
|
logger.info("Template learned successfully");
|
|
102098
102986
|
return template;
|
|
102099
102987
|
}
|
|
102100
|
-
async learnFromStories(storyIds) {
|
|
102988
|
+
async learnFromStories(storyIds, options = {}) {
|
|
102101
102989
|
logger.info(`Learning template from ${storyIds.length} stories: ${storyIds.join(", ")}`);
|
|
102102
102990
|
if (storyIds.length === 0) {
|
|
102103
102991
|
throw new TemplateGenerationError("No story IDs provided");
|
|
102104
102992
|
}
|
|
102105
|
-
const results = await Promise.allSettled(storyIds.map((id) => this.analyzeStory(id)));
|
|
102993
|
+
const results = await Promise.allSettled(storyIds.map((id) => this.analyzeStory(id, options.estimationSource)));
|
|
102106
102994
|
const analyses = [];
|
|
102107
102995
|
const skipped = [];
|
|
102108
102996
|
for (let i3 = 0;i3 < results.length; i3++) {
|
|
@@ -102134,9 +103022,9 @@ class StoryLearner {
|
|
|
102134
103022
|
confidence,
|
|
102135
103023
|
outliers
|
|
102136
103024
|
} = this.learningSession.run(analyses);
|
|
102137
|
-
const template = this.productBuilder.buildMergedTemplate(analyses, mergedTasks);
|
|
103025
|
+
const template = this.productBuilder.buildMergedTemplate(analyses, mergedTasks, options.estimationSource);
|
|
102138
103026
|
const suggestions = this.productBuilder.generateSuggestions(analyses, patterns, confidence, outliers);
|
|
102139
|
-
const variations = this.productBuilder.generateVariations(analyses, patterns, mergedTasks);
|
|
103027
|
+
const variations = this.productBuilder.generateVariations(analyses, patterns, mergedTasks, options.estimationSource);
|
|
102140
103028
|
logger.info(`Multi-story learning complete: ${analyses.length} analyzed, ${skipped.length} skipped, confidence: ${confidence.level} (${confidence.overall}%)`);
|
|
102141
103029
|
return {
|
|
102142
103030
|
analyses,
|
|
@@ -102149,11 +103037,12 @@ class StoryLearner {
|
|
|
102149
103037
|
outliers
|
|
102150
103038
|
};
|
|
102151
103039
|
}
|
|
102152
|
-
async analyzeStory(storyId) {
|
|
102153
|
-
const
|
|
102154
|
-
if (!
|
|
103040
|
+
async analyzeStory(storyId, estimationSource) {
|
|
103041
|
+
const fetched = await this.platform.getWorkItem(storyId);
|
|
103042
|
+
if (!fetched) {
|
|
102155
103043
|
throw new TemplateGenerationError(`Story ${storyId} not found`);
|
|
102156
103044
|
}
|
|
103045
|
+
const story = withEstimationSource(fetched, estimationSource);
|
|
102157
103046
|
const tasks = await this.platform.getChildren(storyId);
|
|
102158
103047
|
const warnings = [];
|
|
102159
103048
|
if (tasks.length === 0) {
|
|
@@ -102168,7 +103057,7 @@ class StoryLearner {
|
|
|
102168
103057
|
if (tasks.length === 1) {
|
|
102169
103058
|
warnings.push(`Story ${storyId} has only 1 task, which may not be representative`);
|
|
102170
103059
|
}
|
|
102171
|
-
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks);
|
|
103060
|
+
const template = this.productBuilder.buildSingleStoryTemplate(story, tasks, estimationSource);
|
|
102172
103061
|
return { story, tasks, template, warnings };
|
|
102173
103062
|
}
|
|
102174
103063
|
extractTitlePattern(taskTitle, storyTitle) {
|
|
@@ -102181,6 +103070,11 @@ class StoryLearner {
|
|
|
102181
103070
|
return this.productBuilder.detectActivity(title, description);
|
|
102182
103071
|
}
|
|
102183
103072
|
}
|
|
103073
|
+
function withEstimationSource(story, source) {
|
|
103074
|
+
if (!source)
|
|
103075
|
+
return story;
|
|
103076
|
+
return { ...story, estimation: readStoryEstimate(story, { kind: "override", field: source }) };
|
|
103077
|
+
}
|
|
102184
103078
|
|
|
102185
103079
|
// src/cli/commands/template/template-create.command.ts
|
|
102186
103080
|
init_template_library();
|
|
@@ -102576,6 +103470,8 @@ mixins: string[] # optional — list of mixin references or file paths (e
|
|
|
102576
103470
|
# Mixins contribute tasks only. Child tasks override mixin tasks with the same id.
|
|
102577
103471
|
# Template names are NOT valid mixin sources.
|
|
102578
103472
|
|
|
103473
|
+
taskType: string # optional — work item type Tasks are created as (e.g. "Task", "Sub-task"). Omit to use the platform default.
|
|
103474
|
+
|
|
102579
103475
|
filter: # required (unless inherited via extends)
|
|
102580
103476
|
workItemTypes: string[] # e.g. ["User Story"], ["Bug"]
|
|
102581
103477
|
states: string[] # e.g. ["New", "Active", "Approved"] — items must be in one of these states
|
|
@@ -102627,7 +103523,16 @@ tasks: # required, minimum 1 item
|
|
|
102627
103523
|
estimation: # optional
|
|
102628
103524
|
strategy: "percentage"
|
|
102629
103525
|
rounding: "nearest" | "up" | "down" | "none" # default "none" — omit unless the user asks for rounding
|
|
102630
|
-
|
|
103526
|
+
normalize: "auto" | "always" | "never" # optional, default "auto" — set "never" only if the user wants percentages used exactly as written
|
|
103527
|
+
minimumTaskEstimate: number # optional, default 0 — minimum per task, in the Task's unit (hours on Azure DevOps)
|
|
103528
|
+
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).
|
|
103529
|
+
conversion: # optional — Story Estimate → Task-unit total before the split. Set exactly ONE of factor or table.
|
|
103530
|
+
factor: number # e.g. 4 when 1 point = 4 hours
|
|
103531
|
+
table: # exact value → amount, e.g. { S: 2, M: 4, L: 5, XL: 13 }
|
|
103532
|
+
multipliers: boolean # optional, only with a table of size keys: "0.3XL" = 0.3 × XL
|
|
103533
|
+
targetFields: string[] # optional — Task fields that receive the estimate, replacing the platform defaults. Rarely needed.
|
|
103534
|
+
ifParentHasNoEstimation: "warn" | "skip" | "use-default" # optional, default "warn" (tasks get blank estimates)
|
|
103535
|
+
defaultParentEstimation: number | string # optional — Story Estimate to assume with use-default, e.g. 5 or "M"
|
|
102631
103536
|
|
|
102632
103537
|
validation: # optional
|
|
102633
103538
|
minTasks: number
|
|
@@ -102672,6 +103577,11 @@ CONSTRAINTS (you must follow these):
|
|
|
102672
103577
|
estimationPercentCondition:
|
|
102673
103578
|
- condition: { field: priority, operator: lte, value: 2 }
|
|
102674
103579
|
percent: 35 # override for critical bugs (priority 1 or 2)
|
|
103580
|
+
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.
|
|
103581
|
+
- Use field reference names and work item types exactly as given in the workspace context; never invent them.
|
|
103582
|
+
- With a conversion table on a picklist source, use the picklist's allowed values exactly as keys and cover every one.
|
|
103583
|
+
- For a numeric scale where hours grow faster than points, use a table with numeric keys instead of a factor.
|
|
103584
|
+
- Conditions on estimation compare the raw Story Estimate: with size categories use equals/not-equals (e.g. value: "L"), never gt/lt/gte/lte.
|
|
102675
103585
|
|
|
102676
103586
|
EXAMPLES:
|
|
102677
103587
|
|
|
@@ -102890,7 +103800,7 @@ ${description}`;
|
|
|
102890
103800
|
Observed patterns from this user's Azure DevOps workspace:
|
|
102891
103801
|
${groundingContext}
|
|
102892
103802
|
|
|
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.
|
|
103803
|
+
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
103804
|
---`;
|
|
102895
103805
|
}
|
|
102896
103806
|
if (prevErrors && prevErrors.length > 0) {
|
|
@@ -103189,7 +104099,7 @@ var STORY_FIELDS = [
|
|
|
103189
104099
|
{ label: "Tags (array)", value: "tags" },
|
|
103190
104100
|
{ label: "Title", value: "title" },
|
|
103191
104101
|
{ label: "State", value: "state" },
|
|
103192
|
-
{ label: "
|
|
104102
|
+
{ label: "Story Estimate (raw value, e.g. points or size)", value: "estimation" },
|
|
103193
104103
|
{ label: "Priority", value: "priority" },
|
|
103194
104104
|
{ label: "Description", value: "description" },
|
|
103195
104105
|
{ label: "Assigned To", value: "assignedTo" },
|
|
@@ -103581,7 +104491,7 @@ Filter Configuration:`));
|
|
|
103581
104491
|
function displayTasksSummary(template) {
|
|
103582
104492
|
output4.print(source_default.bold(`
|
|
103583
104493
|
Tasks (${template.tasks.length}):`));
|
|
103584
|
-
const totalEstimation = template.tasks.reduce((
|
|
104494
|
+
const totalEstimation = template.tasks.reduce((sum2, task) => sum2 + (task.estimationPercent || 0), 0);
|
|
103585
104495
|
template.tasks.forEach((task, index) => {
|
|
103586
104496
|
const percent = task.estimationPercent || 0;
|
|
103587
104497
|
const bar = "■".repeat(Math.round(percent / 5));
|
|
@@ -103679,8 +104589,9 @@ Editing Tasks
|
|
|
103679
104589
|
Editing Estimation Settings
|
|
103680
104590
|
`));
|
|
103681
104591
|
const { configureEstimation: configureEstimation2 } = await Promise.resolve().then(() => (init_template_wizard_helper_command(), exports_template_wizard_helper_command));
|
|
103682
|
-
const estimation = await configureEstimation2(template.estimation);
|
|
104592
|
+
const { estimation, taskType } = await configureEstimation2(template.estimation, ctx.estimationCtx ?? { storyFields: ctx.storyFieldSchemas, workItemTypes: ctx.filterCtx.workItemTypes }, template.taskType);
|
|
103683
104593
|
template.estimation = estimation;
|
|
104594
|
+
template.taskType = taskType;
|
|
103684
104595
|
}).with("validation", async () => {
|
|
103685
104596
|
output4.print(source_default.cyan(`
|
|
103686
104597
|
Editing Validation Rules
|
|
@@ -103745,7 +104656,7 @@ function showStepHint(stepName) {
|
|
|
103745
104656
|
const hints = {
|
|
103746
104657
|
filter: "Tip: Use filters to select which work items this template applies to",
|
|
103747
104658
|
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
|
|
104659
|
+
estimation: "Tip: Choose how Story Estimates turn into Task hours and where they are written",
|
|
103749
104660
|
validation: "Tip: Add constraints to ensure templates are used correctly",
|
|
103750
104661
|
metadata: "Tip: Metadata helps others understand when to use this template"
|
|
103751
104662
|
};
|
|
@@ -103815,7 +104726,7 @@ async function shouldAddMoreTasks(taskCounter) {
|
|
|
103815
104726
|
}));
|
|
103816
104727
|
}
|
|
103817
104728
|
async function handleEstimationNormalization(tasks) {
|
|
103818
|
-
const totalEstimation = tasks.reduce((
|
|
104729
|
+
const totalEstimation = tasks.reduce((sum2, task) => sum2 + (task.estimationPercent || 0), 0);
|
|
103819
104730
|
if (totalEstimation === 100) {
|
|
103820
104731
|
output4.print(source_default.green("✓ Total estimation is 100%"));
|
|
103821
104732
|
return;
|
|
@@ -103943,6 +104854,7 @@ Customize Template
|
|
|
103943
104854
|
const adapter = await createAzureDevOpsAdapter(profile);
|
|
103944
104855
|
const metadataReader = requireProjectMetadataReader(adapter);
|
|
103945
104856
|
const savedQueryReader = requireSavedQueryReader(adapter);
|
|
104857
|
+
const estimationDefaults2 = adapter.getEstimationDefaults();
|
|
103946
104858
|
const [
|
|
103947
104859
|
taskSchemas,
|
|
103948
104860
|
liveWorkItemTypes,
|
|
@@ -103951,7 +104863,7 @@ Customize Template
|
|
|
103951
104863
|
liveTeams,
|
|
103952
104864
|
liveSavedQueries
|
|
103953
104865
|
] = await Promise.all([
|
|
103954
|
-
metadataReader.getFieldSchemas(
|
|
104866
|
+
metadataReader.getFieldSchemas(template.taskType ?? estimationDefaults2.taskWorkItemType),
|
|
103955
104867
|
metadataReader.getWorkItemTypes(),
|
|
103956
104868
|
metadataReader.getAreaPaths(),
|
|
103957
104869
|
metadataReader.getIterationPaths(),
|
|
@@ -103960,6 +104872,7 @@ Customize Template
|
|
|
103960
104872
|
]);
|
|
103961
104873
|
return {
|
|
103962
104874
|
metadataReader,
|
|
104875
|
+
estimationDefaults: estimationDefaults2,
|
|
103963
104876
|
fieldSchemas: taskSchemas,
|
|
103964
104877
|
filterCtx: {
|
|
103965
104878
|
workItemTypes: liveWorkItemTypes,
|
|
@@ -104007,6 +104920,7 @@ Editing Name & Description
|
|
|
104007
104920
|
let filterCtx;
|
|
104008
104921
|
let fieldSchemas;
|
|
104009
104922
|
let adapterForWizard;
|
|
104923
|
+
let estimationDefaults;
|
|
104010
104924
|
try {
|
|
104011
104925
|
const conn = await connectionPromise;
|
|
104012
104926
|
if (!wasAlreadyConnected)
|
|
@@ -104014,6 +104928,7 @@ Editing Name & Description
|
|
|
104014
104928
|
filterCtx = conn.filterCtx;
|
|
104015
104929
|
fieldSchemas = conn.fieldSchemas;
|
|
104016
104930
|
adapterForWizard = conn.metadataReader;
|
|
104931
|
+
estimationDefaults = conn.estimationDefaults;
|
|
104017
104932
|
} catch (err) {
|
|
104018
104933
|
if (!wasAlreadyConnected)
|
|
104019
104934
|
connectSpinner.stop("Connection failed");
|
|
@@ -104025,6 +104940,12 @@ Editing Name & Description
|
|
|
104025
104940
|
}
|
|
104026
104941
|
let storyFieldSchemas = [];
|
|
104027
104942
|
let storySchemasFetched = false;
|
|
104943
|
+
const estimationContext = async () => ({
|
|
104944
|
+
storyFields: storyFieldSchemas.length ? storyFieldSchemas : await adapterForWizard.getFieldSchemas(),
|
|
104945
|
+
workItemTypes: filterCtx.workItemTypes,
|
|
104946
|
+
defaults: estimationDefaults,
|
|
104947
|
+
getTaskFields: (type) => adapterForWizard.getFieldSchemas(type)
|
|
104948
|
+
});
|
|
104028
104949
|
for (const section of ["filter", "tasks", "estimation", "validation", "metadata"]) {
|
|
104029
104950
|
if (!sections.includes(section))
|
|
104030
104951
|
continue;
|
|
@@ -104062,7 +104983,14 @@ Editing Tasks
|
|
|
104062
104983
|
output5.print(source_default.cyan(`
|
|
104063
104984
|
Editing Estimation Settings
|
|
104064
104985
|
`));
|
|
104065
|
-
|
|
104986
|
+
if (!storySchemasFetched) {
|
|
104987
|
+
const wit = template.filter.workItemTypes?.[0];
|
|
104988
|
+
storyFieldSchemas = wit ? await adapterForWizard.getFieldSchemas(wit) : [];
|
|
104989
|
+
storySchemasFetched = true;
|
|
104990
|
+
}
|
|
104991
|
+
const { estimation, taskType } = await configureEstimation(template.estimation, await estimationContext(), template.taskType);
|
|
104992
|
+
template.estimation = estimation;
|
|
104993
|
+
template.taskType = taskType;
|
|
104066
104994
|
break;
|
|
104067
104995
|
}
|
|
104068
104996
|
case "validation": {
|
|
@@ -104103,7 +105031,8 @@ Editing Metadata
|
|
|
104103
105031
|
filterCtx,
|
|
104104
105032
|
fieldSchemas,
|
|
104105
105033
|
storyFieldSchemas,
|
|
104106
|
-
workItemType: template.filter.workItemTypes?.[0]
|
|
105034
|
+
workItemType: template.filter.workItemTypes?.[0],
|
|
105035
|
+
estimationCtx: await estimationContext()
|
|
104107
105036
|
};
|
|
104108
105037
|
const confirmed = await previewTemplate(template, wizardCtx);
|
|
104109
105038
|
if (!confirmed) {
|
|
@@ -104421,7 +105350,7 @@ async function handlePreviewLoop(template, options) {
|
|
|
104421
105350
|
// src/cli/commands/template/template-create.command.ts
|
|
104422
105351
|
init_template_wizard_helper_command();
|
|
104423
105352
|
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) => {
|
|
105353
|
+
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
105354
|
const output9 = createCommandOutput(resolveCommandOutputPolicy({ quiet: options.quiet, verbose: false }));
|
|
104426
105355
|
try {
|
|
104427
105356
|
output9.intro(" Atomize — Template Creator");
|
|
@@ -104680,7 +105609,7 @@ async function createFromStories(options) {
|
|
|
104680
105609
|
const learner = new StoryLearner(learningPlatform);
|
|
104681
105610
|
const learnSpinner = createManagedSpinner();
|
|
104682
105611
|
learnSpinner.start(`Learning from ${storyIds.length} stories...`);
|
|
104683
|
-
const result = await learner.learnFromStories(storyIds);
|
|
105612
|
+
const result = await learner.learnFromStories(storyIds, { estimationSource: options.estimationSource });
|
|
104684
105613
|
learnSpinner.stop(`Analyzed ${result.analyses.length} stories ✓`);
|
|
104685
105614
|
displayMultiStoryResults(result);
|
|
104686
105615
|
if (result.variations.length > 1) {
|
|
@@ -104776,8 +105705,9 @@ Create Template from Scratch
|
|
|
104776
105705
|
const adapter = await createAzureDevOpsAdapter(profile);
|
|
104777
105706
|
const metadataReader = requireProjectMetadataReader(adapter);
|
|
104778
105707
|
const savedQueryReader = requireSavedQueryReader(adapter);
|
|
105708
|
+
const estimationDefaults2 = adapter.getEstimationDefaults();
|
|
104779
105709
|
const [taskSchemas, liveWorkItemTypes, liveAreaPaths, liveIterationPaths, liveTeams, liveSavedQueries] = await Promise.all([
|
|
104780
|
-
metadataReader.getFieldSchemas(
|
|
105710
|
+
metadataReader.getFieldSchemas(estimationDefaults2.taskWorkItemType),
|
|
104781
105711
|
metadataReader.getWorkItemTypes(),
|
|
104782
105712
|
metadataReader.getAreaPaths(),
|
|
104783
105713
|
metadataReader.getIterationPaths(),
|
|
@@ -104786,6 +105716,7 @@ Create Template from Scratch
|
|
|
104786
105716
|
]);
|
|
104787
105717
|
return {
|
|
104788
105718
|
metadataReader,
|
|
105719
|
+
estimationDefaults: estimationDefaults2,
|
|
104789
105720
|
fieldSchemas: taskSchemas,
|
|
104790
105721
|
filterCtx: {
|
|
104791
105722
|
workItemTypes: liveWorkItemTypes,
|
|
@@ -104812,6 +105743,7 @@ Create Template from Scratch
|
|
|
104812
105743
|
let filterCtx;
|
|
104813
105744
|
let fieldSchemas;
|
|
104814
105745
|
let adapterForWizard;
|
|
105746
|
+
let estimationDefaults;
|
|
104815
105747
|
const wasAlreadyConnected = connectionSettled;
|
|
104816
105748
|
const connectSpinner = createManagedSpinner();
|
|
104817
105749
|
if (!wasAlreadyConnected)
|
|
@@ -104823,6 +105755,7 @@ Create Template from Scratch
|
|
|
104823
105755
|
filterCtx = conn.filterCtx;
|
|
104824
105756
|
fieldSchemas = conn.fieldSchemas;
|
|
104825
105757
|
adapterForWizard = conn.metadataReader;
|
|
105758
|
+
estimationDefaults = conn.estimationDefaults;
|
|
104826
105759
|
} catch (err) {
|
|
104827
105760
|
if (!wasAlreadyConnected)
|
|
104828
105761
|
connectSpinner.stop("Connection failed");
|
|
@@ -104870,9 +105803,15 @@ Create Template from Scratch
|
|
|
104870
105803
|
printStep(source_default.blue(`
|
|
104871
105804
|
[${currentStep}/${totalSteps}] Estimation Settings`));
|
|
104872
105805
|
printStep(source_default.gray("████░░"));
|
|
104873
|
-
printStep(source_default.gray(`Tip: Choose how
|
|
105806
|
+
printStep(source_default.gray(`Tip: Choose how Story Estimates turn into Task hours and where they are written
|
|
104874
105807
|
`));
|
|
104875
|
-
const
|
|
105808
|
+
const estimationCtx = {
|
|
105809
|
+
storyFields: storyFieldSchemas.length ? storyFieldSchemas : await adapterForWizard.getFieldSchemas(),
|
|
105810
|
+
workItemTypes: filterCtx.workItemTypes,
|
|
105811
|
+
defaults: estimationDefaults,
|
|
105812
|
+
getTaskFields: (type) => adapterForWizard.getFieldSchemas(type)
|
|
105813
|
+
};
|
|
105814
|
+
const { estimation, taskType } = await configureEstimation(undefined, estimationCtx);
|
|
104876
105815
|
currentStep++;
|
|
104877
105816
|
printStep(source_default.blue(`
|
|
104878
105817
|
[${currentStep}/${totalSteps}] Validation Rules (Optional)`));
|
|
@@ -104912,6 +105851,7 @@ Create Template from Scratch
|
|
|
104912
105851
|
created: new Date().toISOString(),
|
|
104913
105852
|
filter: filterConfig,
|
|
104914
105853
|
tasks,
|
|
105854
|
+
...taskType ? { taskType } : {},
|
|
104915
105855
|
estimation,
|
|
104916
105856
|
validation,
|
|
104917
105857
|
metadata
|
|
@@ -104925,7 +105865,8 @@ Create Template from Scratch
|
|
|
104925
105865
|
filterCtx,
|
|
104926
105866
|
fieldSchemas,
|
|
104927
105867
|
storyFieldSchemas,
|
|
104928
|
-
workItemType
|
|
105868
|
+
workItemType,
|
|
105869
|
+
estimationCtx
|
|
104929
105870
|
};
|
|
104930
105871
|
const confirmed = await previewTemplate(template, wizardCtx);
|
|
104931
105872
|
if (!confirmed) {
|
|
@@ -105711,7 +106652,9 @@ function createValidateCommandDeps(depsOptions) {
|
|
|
105711
106652
|
strict: options.strict === true,
|
|
105712
106653
|
platform: {
|
|
105713
106654
|
getFieldSchemas: (workItemType) => metadataReader.getFieldSchemas(workItemType),
|
|
105714
|
-
listSavedQueries: (folder) => savedQueryReader.listSavedQueries(folder)
|
|
106655
|
+
listSavedQueries: (folder) => savedQueryReader.listSavedQueries(folder),
|
|
106656
|
+
getEstimationDefaults: () => adapter.getEstimationDefaults(),
|
|
106657
|
+
getWorkItemTypes: adapter.getWorkItemTypes?.bind(adapter)
|
|
105715
106658
|
}
|
|
105716
106659
|
},
|
|
105717
106660
|
connectionWarnings: []
|