@sppg2001/atomize 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +221 -46
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -178,6 +178,11 @@ var init_errors = __esm(() => {
|
|
|
178
178
|
};
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// ../atomize-schema/src/estimate-format.ts
|
|
182
|
+
function formatEstimateValue(value) {
|
|
183
|
+
return String(Math.round(value * 100) / 100);
|
|
184
|
+
}
|
|
185
|
+
|
|
181
186
|
// ../atomize-schema/src/estimation-fields.ts
|
|
182
187
|
function matchesFieldPattern(referenceName, patterns = []) {
|
|
183
188
|
return patterns.some((pattern) => pattern.endsWith(".*") ? referenceName.startsWith(pattern.slice(0, -1)) : referenceName === pattern);
|
|
@@ -20112,23 +20117,63 @@ function ownPercentRange(task) {
|
|
|
20112
20117
|
const candidates = [task.estimationPercent ?? 0, ...task.estimationPercentCondition?.map((rule) => rule.percent) ?? []];
|
|
20113
20118
|
return [Math.min(...candidates), Math.max(...candidates)];
|
|
20114
20119
|
}
|
|
20120
|
+
function copyCount(task) {
|
|
20121
|
+
return task.repeat ?? 1;
|
|
20122
|
+
}
|
|
20123
|
+
function generatedTaskCount(tasks) {
|
|
20124
|
+
return tasks.reduce((sum, task) => sum + copyCount(task), 0);
|
|
20125
|
+
}
|
|
20126
|
+
function validateRepeatedTasks(tasks, ctx, maxRepeat) {
|
|
20127
|
+
const repeatedIds = new Set(tasks.filter((t) => t.repeat !== undefined && t.id).map((t) => t.id));
|
|
20128
|
+
tasks.forEach((task, index) => {
|
|
20129
|
+
if (maxRepeat !== undefined && task.repeat !== undefined && task.repeat > maxRepeat) {
|
|
20130
|
+
ctx.addIssue({
|
|
20131
|
+
code: "custom",
|
|
20132
|
+
path: ["tasks", index, "repeat"],
|
|
20133
|
+
message: `Task repeats ${task.repeat} times, but the maximum is ${maxRepeat}. Lower repeat or raise validation.maxRepeat.`,
|
|
20134
|
+
params: { code: "REPEAT_EXCEEDS_MAX", maxRepeat }
|
|
20135
|
+
});
|
|
20136
|
+
}
|
|
20137
|
+
if (task.repeat !== undefined && task.dependsOn?.length) {
|
|
20138
|
+
ctx.addIssue({
|
|
20139
|
+
code: "custom",
|
|
20140
|
+
path: ["tasks", index, "dependsOn"],
|
|
20141
|
+
message: "A repeated task cannot have dependsOn. Remove repeat or dependsOn from this task.",
|
|
20142
|
+
params: { code: "REPEAT_WITH_DEPENDENCY" }
|
|
20143
|
+
});
|
|
20144
|
+
}
|
|
20145
|
+
task.dependsOn?.forEach((depId) => {
|
|
20146
|
+
if (repeatedIds.has(depId)) {
|
|
20147
|
+
ctx.addIssue({
|
|
20148
|
+
code: "custom",
|
|
20149
|
+
path: ["tasks", index, "dependsOn"],
|
|
20150
|
+
message: `Task depends on "${depId}", which is repeated. A task cannot depend on a repeated task.`,
|
|
20151
|
+
params: { code: "DEPENDS_ON_REPEATED_TASK", taskId: depId }
|
|
20152
|
+
});
|
|
20153
|
+
}
|
|
20154
|
+
});
|
|
20155
|
+
});
|
|
20156
|
+
}
|
|
20115
20157
|
function validateTaskConstraints(v, tasks, ctx) {
|
|
20116
|
-
|
|
20117
|
-
|
|
20158
|
+
const count = generatedTaskCount(tasks);
|
|
20159
|
+
const hasRepeats = tasks.some((task) => copyCount(task) > 1);
|
|
20160
|
+
const counted = hasRepeats ? " (counting each repeated copy)" : "";
|
|
20161
|
+
if (v?.minTasks !== undefined && count < v.minTasks) {
|
|
20162
|
+
const needed = v.minTasks - count;
|
|
20118
20163
|
ctx.addIssue({
|
|
20119
20164
|
code: "custom",
|
|
20120
20165
|
path: ["tasks"],
|
|
20121
|
-
message: `Template has ${
|
|
20122
|
-
params: { code: "TOO_FEW_TASKS" }
|
|
20166
|
+
message: `Template has ${count} task(s)${counted}, but minimum is ${v.minTasks}. Add ${needed} more task(s).`,
|
|
20167
|
+
params: { code: "TOO_FEW_TASKS", count, limit: v.minTasks, hasRepeats }
|
|
20123
20168
|
});
|
|
20124
20169
|
}
|
|
20125
|
-
if (v?.maxTasks !== undefined &&
|
|
20126
|
-
const excess =
|
|
20170
|
+
if (v?.maxTasks !== undefined && count > v.maxTasks) {
|
|
20171
|
+
const excess = count - v.maxTasks;
|
|
20127
20172
|
ctx.addIssue({
|
|
20128
20173
|
code: "custom",
|
|
20129
20174
|
path: ["tasks"],
|
|
20130
|
-
message: `Template has ${
|
|
20131
|
-
params: { code: "TOO_MANY_TASKS" }
|
|
20175
|
+
message: `Template has ${count} task(s)${counted}, but maximum is ${v.maxTasks}. Remove ${excess} task(s) or increase maxTasks.`,
|
|
20176
|
+
params: { code: "TOO_MANY_TASKS", count, limit: v.maxTasks, hasRepeats }
|
|
20132
20177
|
});
|
|
20133
20178
|
}
|
|
20134
20179
|
}
|
|
@@ -20150,14 +20195,14 @@ function validateUniqueTaskIds(tasks, ctx) {
|
|
|
20150
20195
|
});
|
|
20151
20196
|
});
|
|
20152
20197
|
}
|
|
20153
|
-
function validateEstimationConstraints(v, totalPercent, maxTotalPercent, ctx) {
|
|
20198
|
+
function validateEstimationConstraints(v, totalPercent, maxTotalPercent, hasRepeats, ctx) {
|
|
20154
20199
|
if (v?.totalEstimationMustBe !== undefined) {
|
|
20155
20200
|
if (v.totalEstimationMustBe < totalPercent || v.totalEstimationMustBe > maxTotalPercent) {
|
|
20156
20201
|
ctx.addIssue({
|
|
20157
20202
|
code: "custom",
|
|
20158
20203
|
path: ["tasks"],
|
|
20159
20204
|
message: `Total estimation is ${totalPercent}%, but must be ${v.totalEstimationMustBe}%.`,
|
|
20160
|
-
params: { code: "INVALID_TOTAL_ESTIMATION" }
|
|
20205
|
+
params: { code: "INVALID_TOTAL_ESTIMATION", hasRepeats }
|
|
20161
20206
|
});
|
|
20162
20207
|
}
|
|
20163
20208
|
} else if (v?.totalEstimationRange) {
|
|
@@ -20167,7 +20212,7 @@ function validateEstimationConstraints(v, totalPercent, maxTotalPercent, ctx) {
|
|
|
20167
20212
|
code: "custom",
|
|
20168
20213
|
path: ["tasks"],
|
|
20169
20214
|
message: `Total estimation is ${totalPercent}%, but must be between ${min}% and ${max}%.`,
|
|
20170
|
-
params: { code: "INVALID_ESTIMATION_RANGE" }
|
|
20215
|
+
params: { code: "INVALID_ESTIMATION_RANGE", hasRepeats }
|
|
20171
20216
|
});
|
|
20172
20217
|
}
|
|
20173
20218
|
}
|
|
@@ -20205,7 +20250,7 @@ function reportCircularDependencies(tasks, taskIndexById, ctx) {
|
|
|
20205
20250
|
});
|
|
20206
20251
|
}
|
|
20207
20252
|
}
|
|
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";
|
|
20253
|
+
var DATE_OR_MACRO_RE, DateOrMacroSchema, REFERENCE_NAME_RE, ConditionOperatorSchema, ConditionValueSchema, MULTI_VALUE_STANDARD_FIELDS, MULTI_VALUE_FIELD_OPERATORS, ConditionSchema, SavedQuerySchema, FilterCriteriaSchema, EstimationPercentConditionSchema, TaskDefinitionSchema, NUMBER_PATTERN, EstimationConversionSchema, EstimationConfigSchema, ValidationModeSchema, ValidationConfigSchema, MetadataSchema, TaskTemplateBaseSchema, ExtendingTaskTemplateSchema, TaskTemplateSchema, DEFAULT_MAX_REPEAT = 20, MixinTemplateSchema, CURRENT_ITERATION = "@CurrentIteration", TEAM_AREAS = "@TeamAreas";
|
|
20209
20254
|
var init_schema = __esm(() => {
|
|
20210
20255
|
init_zod();
|
|
20211
20256
|
DATE_OR_MACRO_RE = /^(@Today|@StartOfDay|@StartOfMonth|@StartOfWeek|@StartOfYear)(\s*[+-]\s*\d+)?$|^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/i;
|
|
@@ -20299,6 +20344,7 @@ var init_schema = __esm(() => {
|
|
|
20299
20344
|
tags: exports_external.array(exports_external.string()).describe("Tags applied to the generated task.").optional(),
|
|
20300
20345
|
condition: ConditionSchema.describe("Condition evaluated against the parent story. If it does not match, this task is skipped.").optional(),
|
|
20301
20346
|
dependsOn: exports_external.array(exports_external.string()).describe("IDs of tasks this task depends on. Atomize creates predecessor dependency links between these tasks.").optional(),
|
|
20347
|
+
repeat: exports_external.number().int("Repeat count must be a whole number").min(1, "Repeat count must be at least 1").describe("Number of identical Tasks to generate from this definition (default 1). Each copy's title gets its position, e.g. 'Review (2)'. Cannot be combined with dependsOn.").optional(),
|
|
20302
20348
|
assignTo: exports_external.string().describe("Email address or alias to assign this task to.").optional(),
|
|
20303
20349
|
priority: exports_external.number().describe("Priority value for the generated task.").optional(),
|
|
20304
20350
|
activity: exports_external.string().describe("Azure DevOps activity type for this task (e.g. 'Development', 'Testing', 'Design').").optional(),
|
|
@@ -20365,6 +20411,7 @@ var init_schema = __esm(() => {
|
|
|
20365
20411
|
}).strict().describe("Sum of all non-conditional task percentages must fall within this range; bounds may exceed 100 when tasks span roles.").optional(),
|
|
20366
20412
|
minTasks: exports_external.number().describe("Minimum number of tasks the template must define.").optional(),
|
|
20367
20413
|
maxTasks: exports_external.number().describe("Maximum number of tasks the template may define.").optional(),
|
|
20414
|
+
maxRepeat: exports_external.number().int("Maximum repeat count must be a whole number").min(1, "Maximum repeat count must be at least 1").describe("Highest 'repeat' any task in this template may use (default 20). Raise it for a template that genuinely needs more copies.").optional(),
|
|
20368
20415
|
taskEstimationRange: exports_external.object({
|
|
20369
20416
|
min: exports_external.number().describe("Minimum allowed estimation percentage for a single task."),
|
|
20370
20417
|
max: exports_external.number().describe("Maximum allowed estimation percentage for a single task.")
|
|
@@ -20419,12 +20466,13 @@ var init_schema = __esm(() => {
|
|
|
20419
20466
|
let maxTotalPercent = 0;
|
|
20420
20467
|
for (const task of tasks) {
|
|
20421
20468
|
const [ownMin, ownMax] = ownPercentRange(task);
|
|
20422
|
-
|
|
20423
|
-
|
|
20469
|
+
const copies = copyCount(task);
|
|
20470
|
+
totalPercent += task.condition ? 0 : ownMin * copies;
|
|
20471
|
+
maxTotalPercent += ownMax * copies;
|
|
20424
20472
|
}
|
|
20425
20473
|
const taskIds = new Set(tasks.map((t) => t.id).filter(Boolean));
|
|
20426
20474
|
validateUniqueTaskIds(tasks, ctx);
|
|
20427
|
-
validateEstimationConstraints(v, totalPercent, maxTotalPercent, ctx);
|
|
20475
|
+
validateEstimationConstraints(v, totalPercent, maxTotalPercent, tasks.some((t) => copyCount(t) > 1), ctx);
|
|
20428
20476
|
validateTaskConstraints(v, tasks, ctx);
|
|
20429
20477
|
validateRequiredTasks(v, tasks, ctx);
|
|
20430
20478
|
const availableIds = Array.from(taskIds);
|
|
@@ -20433,6 +20481,7 @@ var init_schema = __esm(() => {
|
|
|
20433
20481
|
if (t.id)
|
|
20434
20482
|
taskIndexById.set(t.id, i);
|
|
20435
20483
|
});
|
|
20484
|
+
validateRepeatedTasks(tasks, ctx, v?.maxRepeat ?? DEFAULT_MAX_REPEAT);
|
|
20436
20485
|
tasks.forEach((task, index) => {
|
|
20437
20486
|
task.dependsOn?.forEach((depId) => {
|
|
20438
20487
|
if (!taskIds.has(depId)) {
|
|
@@ -20455,6 +20504,7 @@ var init_schema = __esm(() => {
|
|
|
20455
20504
|
origin: exports_external.string().describe("Catalog ref (template:<name> or mixin:<name>) this mixin was derived from. Informational only — does not affect ref resolution.").optional()
|
|
20456
20505
|
}).strict().superRefine((data, ctx) => {
|
|
20457
20506
|
validateUniqueTaskIds(data.tasks, ctx);
|
|
20507
|
+
validateRepeatedTasks(data.tasks, ctx);
|
|
20458
20508
|
});
|
|
20459
20509
|
});
|
|
20460
20510
|
|
|
@@ -21270,16 +21320,32 @@ function totalEstimationPercent(tasks) {
|
|
|
21270
21320
|
return tasks.reduce((sum, task) => sum + (task.estimationPercent ?? 0), 0);
|
|
21271
21321
|
}
|
|
21272
21322
|
function totalUnconditionalEstimationPercent(tasks) {
|
|
21273
|
-
return tasks.filter((task) => !task.condition).reduce((sum, task) => sum + (task.estimationPercent ?? 0), 0);
|
|
21323
|
+
return tasks.filter((task) => !task.condition).reduce((sum, task) => sum + (task.estimationPercent ?? 0) * copyCount(task), 0);
|
|
21274
21324
|
}
|
|
21275
21325
|
function shouldOfferOverageNormalization(tasks) {
|
|
21276
21326
|
const total = totalEstimationPercent(tasks);
|
|
21277
21327
|
return { shouldOffer: total > 100, total };
|
|
21278
21328
|
}
|
|
21279
21329
|
var init_estimation_distribution = __esm(() => {
|
|
21330
|
+
init_schema2();
|
|
21280
21331
|
init_estimation_normalizer();
|
|
21281
21332
|
});
|
|
21282
21333
|
|
|
21334
|
+
// ../atomize-core/src/core/task-repeater.ts
|
|
21335
|
+
function repeatTasks(tasks) {
|
|
21336
|
+
return tasks.flatMap((task) => {
|
|
21337
|
+
const iterationCount = task.repeat ?? 1;
|
|
21338
|
+
return Array.from({ length: iterationCount }, (_2, index) => ({
|
|
21339
|
+
task,
|
|
21340
|
+
iteration: index + 1,
|
|
21341
|
+
iterationCount
|
|
21342
|
+
}));
|
|
21343
|
+
});
|
|
21344
|
+
}
|
|
21345
|
+
function withOrdinal(title, { iteration, iterationCount }) {
|
|
21346
|
+
return iterationCount > 1 ? `${title} (${iteration})` : title;
|
|
21347
|
+
}
|
|
21348
|
+
|
|
21283
21349
|
// ../atomize-core/src/core/template-interpolator.ts
|
|
21284
21350
|
function interpolateValue(template, story, onMissing) {
|
|
21285
21351
|
return template.replace(/\${story\.title}/g, story.title).replace(/\${story\.id}/g, story.id).replace(/\${story\.description}/g, story.description ?? "").replace(/\{\{\s*story\.customFields\['([^']+)'\]\s*\}\}/g, (_match, referenceName) => {
|
|
@@ -21314,8 +21380,9 @@ class EstimationCalculator {
|
|
|
21314
21380
|
if (resolution.kind === "blank") {
|
|
21315
21381
|
logger.warn(`Story ${story.id}: ${resolution.reason}. Task Estimates will be left blank.`);
|
|
21316
21382
|
}
|
|
21317
|
-
const calculatedTasks = [];
|
|
21318
21383
|
const skippedTasks = [];
|
|
21384
|
+
const activeTasks = [];
|
|
21385
|
+
const resolvedPercents = new Map;
|
|
21319
21386
|
for (const templateTask of templateTasks) {
|
|
21320
21387
|
logger.debug(`Processing template task: ${templateTask.title} (Condition: ${templateTask.condition ? JSON.stringify(templateTask.condition) : "none"})`);
|
|
21321
21388
|
if (templateTask.condition) {
|
|
@@ -21345,9 +21412,14 @@ class EstimationCalculator {
|
|
|
21345
21412
|
skippedTasks.push({ templateTask, reason });
|
|
21346
21413
|
continue;
|
|
21347
21414
|
}
|
|
21348
|
-
|
|
21349
|
-
|
|
21415
|
+
activeTasks.push(templateTask);
|
|
21416
|
+
resolvedPercents.set(templateTask, resolvedPercent);
|
|
21350
21417
|
}
|
|
21418
|
+
const calculatedTasks = repeatTasks(activeTasks).map((copy) => {
|
|
21419
|
+
const calculatedTask = this.buildCalculatedTask(copy.task, story, connectUserEmail, 0, resolvedPercents.get(copy.task));
|
|
21420
|
+
calculatedTask.title = withOrdinal(calculatedTask.title, copy);
|
|
21421
|
+
return calculatedTask;
|
|
21422
|
+
});
|
|
21351
21423
|
if (fieldMapping) {
|
|
21352
21424
|
for (const task of calculatedTasks) {
|
|
21353
21425
|
task.workItemType = fieldMapping.taskWorkItemType;
|
|
@@ -89478,7 +89550,7 @@ class TemplateValidator {
|
|
|
89478
89550
|
collectWarnings(template) {
|
|
89479
89551
|
const warnings = [];
|
|
89480
89552
|
const v2 = template.validation;
|
|
89481
|
-
const total = template.tasks.reduce((sum, t2) => sum + (t2.estimationPercent ?? 0), 0);
|
|
89553
|
+
const total = template.tasks.reduce((sum, t2) => sum + (t2.estimationPercent ?? 0) * copyCount(t2), 0);
|
|
89482
89554
|
const hasStrictRule = v2?.totalEstimationMustBe !== undefined || v2?.totalEstimationRange !== undefined;
|
|
89483
89555
|
const normalize = template.estimation?.normalize ?? "auto";
|
|
89484
89556
|
if (!hasStrictRule && total > 100 && normalize !== "always") {
|
|
@@ -89604,22 +89676,32 @@ class TemplateValidator {
|
|
|
89604
89676
|
return this.handleZodError(err);
|
|
89605
89677
|
}
|
|
89606
89678
|
handleDomainError(err, code) {
|
|
89679
|
+
const params = err.code === "custom" ? err.params : undefined;
|
|
89680
|
+
const repeatHint = params?.hasRepeats ? " Each copy of a repeated task counts toward this total." : "";
|
|
89607
89681
|
switch (code) {
|
|
89608
89682
|
case "INVALID_TOTAL_ESTIMATION":
|
|
89609
|
-
return this.handleNumericError(err.message, /is (\d+)%, but must be (\d+)%/, (current, required2) => {
|
|
89683
|
+
return this.withHint(repeatHint, this.handleNumericError(err.message, /is (\d+)%, but must be (\d+)%/, (current, required2) => {
|
|
89610
89684
|
const diff = required2 - current;
|
|
89611
89685
|
return diff > 0 ? `Add ${diff}% to existing tasks or create a new task with ${diff}% estimation.` : `Reduce task estimations by ${Math.abs(diff)}% to reach ${required2}%.`;
|
|
89612
|
-
});
|
|
89686
|
+
}));
|
|
89613
89687
|
case "INVALID_ESTIMATION_RANGE":
|
|
89614
|
-
return this.handleNumericError(err.message, /is (\d+)%, but must be between (\d+)% and (\d+)%/, (current, min, max) => {
|
|
89688
|
+
return this.withHint(repeatHint, this.handleNumericError(err.message, /is (\d+)%, but must be between (\d+)% and (\d+)%/, (current, min, max) => {
|
|
89615
89689
|
if (current < min)
|
|
89616
89690
|
return `Increase task estimations by ${min - current}% to meet the minimum of ${min}%.`;
|
|
89617
89691
|
return `Reduce task estimations by ${current - max}% to stay within the maximum of ${max}%.`;
|
|
89618
|
-
});
|
|
89619
|
-
case "TOO_FEW_TASKS":
|
|
89620
|
-
|
|
89621
|
-
|
|
89622
|
-
|
|
89692
|
+
}));
|
|
89693
|
+
case "TOO_FEW_TASKS": {
|
|
89694
|
+
if (typeof params?.count !== "number" || typeof params.limit !== "number")
|
|
89695
|
+
return;
|
|
89696
|
+
const { count, limit } = params;
|
|
89697
|
+
return params.hasRepeats ? `Add ${limit - count} more task(s) or raise a repeat count to meet the minimum requirement of ${limit} tasks.` : `Add ${limit - count} more task(s) to meet the minimum requirement of ${limit} tasks.`;
|
|
89698
|
+
}
|
|
89699
|
+
case "TOO_MANY_TASKS": {
|
|
89700
|
+
if (typeof params?.count !== "number" || typeof params.limit !== "number")
|
|
89701
|
+
return;
|
|
89702
|
+
const { count, limit } = params;
|
|
89703
|
+
return params.hasRepeats ? `Remove ${count - limit} task(s), lower a repeat count, or increase the maxTasks limit to ${count}.` : `Remove ${count - limit} task(s) or increase the maxTasks limit to ${count}.`;
|
|
89704
|
+
}
|
|
89623
89705
|
case "INVALID_DEPENDENCY": {
|
|
89624
89706
|
const match = err.message.match(/non-existent task ID: "([^"]+)"/);
|
|
89625
89707
|
return match ? `Either add a task with id: "${match[1]}" or update the dependsOn field to reference an existing task ID.` : undefined;
|
|
@@ -89632,6 +89714,14 @@ class TemplateValidator {
|
|
|
89632
89714
|
}
|
|
89633
89715
|
return "Remove one of the dependencies in the cycle to break the circular reference.";
|
|
89634
89716
|
}
|
|
89717
|
+
case "REPEAT_EXCEEDS_MAX":
|
|
89718
|
+
return "Lower the repeat count, or raise validation.maxRepeat if the template genuinely needs more copies.";
|
|
89719
|
+
case "REPEAT_WITH_DEPENDENCY":
|
|
89720
|
+
return "Remove dependsOn from the repeated task, or remove repeat and define each copy as its own task.";
|
|
89721
|
+
case "DEPENDS_ON_REPEATED_TASK": {
|
|
89722
|
+
const match = err.message.match(/depends on "([^"]+)"/);
|
|
89723
|
+
return match ? `Remove "${match[1]}" from dependsOn, or remove repeat from task "${match[1]}".` : "Remove the dependency on the repeated task, or remove its repeat.";
|
|
89724
|
+
}
|
|
89635
89725
|
case "MISSING_REQUIRED_TASK": {
|
|
89636
89726
|
const match = err.message.match(/title "([^"]+)"/);
|
|
89637
89727
|
return match ? `Add a task with title "${match[1]}" to satisfy the required tasks constraint.` : "Add the missing required task to the template.";
|
|
@@ -89640,6 +89730,9 @@ class TemplateValidator {
|
|
|
89640
89730
|
return;
|
|
89641
89731
|
}
|
|
89642
89732
|
}
|
|
89733
|
+
withHint(hint, suggestion) {
|
|
89734
|
+
return suggestion && `${suggestion}${hint}`;
|
|
89735
|
+
}
|
|
89643
89736
|
handleNumericError(message, regex, formatter) {
|
|
89644
89737
|
const values = this.extractValues(message, regex);
|
|
89645
89738
|
if (!values || values.length === 0)
|
|
@@ -93384,6 +93477,12 @@ var init_prompt_utilities = __esm(() => {
|
|
|
93384
93477
|
return required2;
|
|
93385
93478
|
return Validators.maxLength(fieldName, maxLength)(input2);
|
|
93386
93479
|
},
|
|
93480
|
+
positiveInteger: (fieldName) => (input2) => {
|
|
93481
|
+
const num = Number(input2);
|
|
93482
|
+
if (!input2 || input2.trim() === "" || !Number.isInteger(num) || num < 1)
|
|
93483
|
+
return `${fieldName} must be a whole number of at least 1`;
|
|
93484
|
+
return;
|
|
93485
|
+
},
|
|
93387
93486
|
positiveNumber: (fieldName) => (input2) => {
|
|
93388
93487
|
const num = Number(input2);
|
|
93389
93488
|
if (!input2 || input2.trim() === "" || Number.isNaN(num))
|
|
@@ -98679,7 +98778,7 @@ var program = new Command;
|
|
|
98679
98778
|
|
|
98680
98779
|
// package.json
|
|
98681
98780
|
var name = "@sppg2001/atomize";
|
|
98682
|
-
var version2 = "4.
|
|
98781
|
+
var version2 = "4.2.0";
|
|
98683
98782
|
|
|
98684
98783
|
// src/cli/commands/auth/auth-add.command.ts
|
|
98685
98784
|
init_dist6();
|
|
@@ -99635,6 +99734,7 @@ init_dist6();
|
|
|
99635
99734
|
init_logger2();
|
|
99636
99735
|
init_estimation_distribution();
|
|
99637
99736
|
init_platform_factory();
|
|
99737
|
+
init_schema2();
|
|
99638
99738
|
init_template_library();
|
|
99639
99739
|
init_errors();
|
|
99640
99740
|
|
|
@@ -100187,7 +100287,7 @@ function printReport(report, options, dryRun, storyIds) {
|
|
|
100187
100287
|
continue;
|
|
100188
100288
|
}
|
|
100189
100289
|
output2.print(source_default.green(`✓ ${sanitizeTty(result.story.id)}: ${sanitizeTty(result.story.title)}`));
|
|
100190
|
-
const withUnit = (value) => result.estimateUnit ? `${value} ${result.estimateUnit}` :
|
|
100290
|
+
const withUnit = (value) => result.estimateUnit ? `${formatEstimateValue(value)} ${result.estimateUnit}` : formatEstimateValue(value);
|
|
100191
100291
|
const rawEstimate = result.storyEstimate;
|
|
100192
100292
|
const converted = result.estimationSummary?.storyEstimation;
|
|
100193
100293
|
const conversionNote = converted !== undefined && converted !== rawEstimate ? ` (converted to ${withUnit(converted)})` : "";
|
|
@@ -103502,6 +103602,7 @@ tasks: # required, minimum 1 item
|
|
|
103502
103602
|
tags: string[] # optional
|
|
103503
103603
|
id: string # optional, required if this task is referenced in dependsOn
|
|
103504
103604
|
dependsOn: string[] # optional, references other task ids
|
|
103605
|
+
repeat: number # optional, default 1 — create this many identical Tasks from this one definition (see rule 9)
|
|
103505
103606
|
condition: # optional — skip this task entirely when condition is false
|
|
103506
103607
|
# Standard fields (case-sensitive, always lowercase camelCase — NOT the ADO REST field
|
|
103507
103608
|
# names like "System.State" or "Microsoft.VSTS.Common.Priority"):
|
|
@@ -103535,8 +103636,9 @@ estimation: # optional
|
|
|
103535
103636
|
defaultParentEstimation: number | string # optional — Story Estimate to assume with use-default, e.g. 5 or "M"
|
|
103536
103637
|
|
|
103537
103638
|
validation: # optional
|
|
103538
|
-
minTasks: number
|
|
103639
|
+
minTasks: number # each copy of a repeated task counts
|
|
103539
103640
|
maxTasks: number
|
|
103641
|
+
maxRepeat: number # optional, default 20 — the highest repeat any task may use
|
|
103540
103642
|
totalEstimationMustBe: number # usually 100
|
|
103541
103643
|
totalEstimationRange:
|
|
103542
103644
|
min: number
|
|
@@ -103551,7 +103653,7 @@ metadata: # optional
|
|
|
103551
103653
|
|
|
103552
103654
|
CONSTRAINTS (you must follow these):
|
|
103553
103655
|
1. Estimation sums:
|
|
103554
|
-
- Simple templates (no task-level conditions): estimationPercent values MUST sum to exactly 100.
|
|
103656
|
+
- Simple templates (no task-level conditions): estimationPercent values MUST sum to exactly 100. A repeated task contributes its estimationPercent once per copy.
|
|
103555
103657
|
- Multi-archetype templates (tasks use conditions to target different story types): each archetype's active task set should sum to ~100% independently. The total across all declared tasks will naturally exceed 100 — this is expected and valid.
|
|
103556
103658
|
2. If a task uses dependsOn, all referenced IDs must exist on other tasks in the same template.
|
|
103557
103659
|
3. workItemTypes values are case-sensitive strings matching ADO work item types.
|
|
@@ -103582,6 +103684,12 @@ CONSTRAINTS (you must follow these):
|
|
|
103582
103684
|
- With a conversion table on a picklist source, use the picklist's allowed values exactly as keys and cover every one.
|
|
103583
103685
|
- For a numeric scale where hours grow faster than points, use a table with numeric keys instead of a factor.
|
|
103584
103686
|
- Conditions on estimation compare the raw Story Estimate: with size categories use equals/not-equals (e.g. value: "L"), never gt/lt/gte/lte.
|
|
103687
|
+
9. REPEATED TASKS — when the user wants several identical Tasks from the same definition (e.g. "one review task per developer, 4 developers"), write ONE task with repeat: N instead of copying the task block N times.
|
|
103688
|
+
- Atomize titles the copies "<title> (1)", "<title> (2)", ... so do not number the title yourself.
|
|
103689
|
+
- Each copy gets the task's full estimationPercent, so every copy counts toward the total: repeat: 4 at 10% contributes 40%.
|
|
103690
|
+
- NEVER combine repeat with dependsOn, and never list a repeated task's id in another task's dependsOn.
|
|
103691
|
+
- repeat must be a whole number, at most 20 unless you also set validation.maxRepeat to at least that number. Only raise maxRepeat when the user explicitly asks for more than 20 copies.
|
|
103692
|
+
- Every copy is identical; when copies need different assignees, estimates or fields, write separate tasks instead.
|
|
103585
103693
|
|
|
103586
103694
|
EXAMPLES:
|
|
103587
103695
|
|
|
@@ -103785,7 +103893,32 @@ filter:
|
|
|
103785
103893
|
tasks:
|
|
103786
103894
|
- id: "implement"
|
|
103787
103895
|
title: "Implement (Mobile): \${story.title}"
|
|
103788
|
-
estimationPercent: 60
|
|
103896
|
+
estimationPercent: 60
|
|
103897
|
+
|
|
103898
|
+
Example 7 — Repeated identical tasks (one review per developer):
|
|
103899
|
+
version: "1.0"
|
|
103900
|
+
name: "Team Review"
|
|
103901
|
+
description: "Implementation plus one review conclusions task for each of the four developers."
|
|
103902
|
+
filter:
|
|
103903
|
+
workItemTypes: ["User Story"]
|
|
103904
|
+
states: ["Active"]
|
|
103905
|
+
excludeIfHasTasks: true
|
|
103906
|
+
tasks:
|
|
103907
|
+
- title: "Implementation"
|
|
103908
|
+
estimationPercent: 60
|
|
103909
|
+
activity: "Development"
|
|
103910
|
+
assignTo: "@ParentAssignee"
|
|
103911
|
+
- title: "Add review conclusions"
|
|
103912
|
+
estimationPercent: 10
|
|
103913
|
+
repeat: 4
|
|
103914
|
+
activity: "Development"
|
|
103915
|
+
assignTo: "@Unassigned"
|
|
103916
|
+
estimation:
|
|
103917
|
+
strategy: "percentage"
|
|
103918
|
+
metadata:
|
|
103919
|
+
category: "Agile"
|
|
103920
|
+
difficulty: "beginner"
|
|
103921
|
+
estimationGuidelines: "60% + 4 copies × 10% = 100%."`;
|
|
103789
103922
|
function buildSystemPrompt() {
|
|
103790
103923
|
return SYSTEM_PROMPT;
|
|
103791
103924
|
}
|
|
@@ -103854,6 +103987,7 @@ init_prompt_utilities();
|
|
|
103854
103987
|
|
|
103855
103988
|
// src/cli/commands/template/template-wizard.ts
|
|
103856
103989
|
init_dist6();
|
|
103990
|
+
init_schema2();
|
|
103857
103991
|
init_errors();
|
|
103858
103992
|
init_estimation_normalizer();
|
|
103859
103993
|
init_source();
|
|
@@ -104348,12 +104482,22 @@ async function configureTaskTags(defaults) {
|
|
|
104348
104482
|
return feature.data.length > 0 ? feature.data : undefined;
|
|
104349
104483
|
}
|
|
104350
104484
|
async function configureAdvancedTaskOptions(storyFieldSchemas, defaults) {
|
|
104351
|
-
const
|
|
104352
|
-
message: "
|
|
104353
|
-
placeholder: "
|
|
104354
|
-
initialValue: defaults?.
|
|
104485
|
+
const repeatRaw = assertNotCancelled(await text({
|
|
104486
|
+
message: "How many identical Tasks should this create?",
|
|
104487
|
+
placeholder: "1",
|
|
104488
|
+
initialValue: String(defaults?.repeat ?? 1),
|
|
104489
|
+
validate: Validators.positiveInteger("Number of Tasks")
|
|
104355
104490
|
}));
|
|
104356
|
-
const
|
|
104491
|
+
const repeat = Number(repeatRaw);
|
|
104492
|
+
let dependsOn = [];
|
|
104493
|
+
if (repeat === 1) {
|
|
104494
|
+
const dependsOnRaw = assertNotCancelled(await text({
|
|
104495
|
+
message: "Depends on task IDs (comma-separated, optional):",
|
|
104496
|
+
placeholder: "e.g. task-setup, task-db, task-build, task-test",
|
|
104497
|
+
initialValue: defaults?.dependsOn?.join(", ") ?? ""
|
|
104498
|
+
}));
|
|
104499
|
+
dependsOn = Filters.commaSeparated(dependsOnRaw);
|
|
104500
|
+
}
|
|
104357
104501
|
const addCondition = assertNotCancelled(await confirm({ message: "Add a task condition?", initialValue: false }));
|
|
104358
104502
|
let condition;
|
|
104359
104503
|
if (addCondition) {
|
|
@@ -104366,6 +104510,8 @@ async function configureAdvancedTaskOptions(storyFieldSchemas, defaults) {
|
|
|
104366
104510
|
validate: Validators.priorityRange
|
|
104367
104511
|
}));
|
|
104368
104512
|
const result = {};
|
|
104513
|
+
if (repeat > 1)
|
|
104514
|
+
result.repeat = repeat;
|
|
104369
104515
|
if (dependsOn.length > 0)
|
|
104370
104516
|
result.dependsOn = dependsOn;
|
|
104371
104517
|
if (condition)
|
|
@@ -104409,6 +104555,8 @@ async function buildTaskDefinition(isFirstTask, includeAdvanced, fieldSchemas, s
|
|
|
104409
104555
|
}
|
|
104410
104556
|
const advanced = await configureAdvancedTaskOptions(storyFieldSchemas, defaults);
|
|
104411
104557
|
Object.assign(taskDef, advanced);
|
|
104558
|
+
} else if (defaults?.repeat !== undefined) {
|
|
104559
|
+
taskDef.repeat = defaults.repeat;
|
|
104412
104560
|
}
|
|
104413
104561
|
const customFields = await configureCustomFields(fieldSchemas, storyFieldSchemas);
|
|
104414
104562
|
if (Object.keys(customFields).length > 0) {
|
|
@@ -104437,6 +104585,7 @@ function normalizeEstimations(tasks) {
|
|
|
104437
104585
|
}
|
|
104438
104586
|
}
|
|
104439
104587
|
async function previewTemplate(template, ctx) {
|
|
104588
|
+
await offerRepeatLimit(template);
|
|
104440
104589
|
displayTemplatePreview(template);
|
|
104441
104590
|
const action = assertNotCancelled(await select({
|
|
104442
104591
|
message: "What would you like to do?",
|
|
@@ -104449,6 +104598,21 @@ async function previewTemplate(template, ctx) {
|
|
|
104449
104598
|
}));
|
|
104450
104599
|
return await handlePreviewAction(action, template, ctx);
|
|
104451
104600
|
}
|
|
104601
|
+
async function offerRepeatLimit(template) {
|
|
104602
|
+
const highest = Math.max(...template.tasks.map(copyCount));
|
|
104603
|
+
const limit = template.validation?.maxRepeat ?? DEFAULT_MAX_REPEAT;
|
|
104604
|
+
if (highest <= limit)
|
|
104605
|
+
return;
|
|
104606
|
+
const raise = assertNotCancelled(await confirm({
|
|
104607
|
+
message: `A task creates ${highest} copies, above the limit of ${limit}. Raise the limit (validation.maxRepeat) to ${highest}?`,
|
|
104608
|
+
initialValue: true
|
|
104609
|
+
}));
|
|
104610
|
+
if (raise)
|
|
104611
|
+
template.validation = { ...template.validation, maxRepeat: highest };
|
|
104612
|
+
}
|
|
104613
|
+
function totalTaskPercent(tasks) {
|
|
104614
|
+
return tasks.reduce((sum2, task) => sum2 + (task.estimationPercent || 0) * copyCount(task), 0);
|
|
104615
|
+
}
|
|
104452
104616
|
function displayTemplatePreview(template) {
|
|
104453
104617
|
output4.print(source_default.cyan(`
|
|
104454
104618
|
Template Preview
|
|
@@ -104489,13 +104653,16 @@ Filter Configuration:`));
|
|
|
104489
104653
|
}
|
|
104490
104654
|
}
|
|
104491
104655
|
function displayTasksSummary(template) {
|
|
104656
|
+
const generated = template.tasks.reduce((sum2, task) => sum2 + copyCount(task), 0);
|
|
104657
|
+
const counted = generated === template.tasks.length ? "" : `, ${generated} generated`;
|
|
104492
104658
|
output4.print(source_default.bold(`
|
|
104493
|
-
Tasks (${template.tasks.length}):`));
|
|
104494
|
-
const totalEstimation = template.tasks
|
|
104659
|
+
Tasks (${template.tasks.length}${counted}):`));
|
|
104660
|
+
const totalEstimation = totalTaskPercent(template.tasks);
|
|
104495
104661
|
template.tasks.forEach((task, index) => {
|
|
104496
104662
|
const percent = task.estimationPercent || 0;
|
|
104497
104663
|
const bar = "■".repeat(Math.round(percent / 5));
|
|
104498
|
-
|
|
104664
|
+
const copies = copyCount(task) > 1 ? source_default.cyan(` ×${copyCount(task)}`) : "";
|
|
104665
|
+
output4.print(` ${index + 1}. ${task.title}${copies} ${source_default.gray(`[${percent}%]`)} ${source_default.green(bar)}`);
|
|
104499
104666
|
if (task.description) {
|
|
104500
104667
|
output4.print(source_default.gray(` ${task.description}`));
|
|
104501
104668
|
}
|
|
@@ -104525,6 +104692,9 @@ Validation Rules:`));
|
|
|
104525
104692
|
if (template.validation.minTasks || template.validation.maxTasks) {
|
|
104526
104693
|
output4.print(` Task count: ${template.validation.minTasks || "no min"} - ${template.validation.maxTasks || "no max"}`);
|
|
104527
104694
|
}
|
|
104695
|
+
if (template.validation.maxRepeat !== undefined) {
|
|
104696
|
+
output4.print(` Copies per task: up to ${template.validation.maxRepeat}`);
|
|
104697
|
+
}
|
|
104528
104698
|
}
|
|
104529
104699
|
function displayMetadata(template) {
|
|
104530
104700
|
if (!template.metadata)
|
|
@@ -104726,13 +104896,17 @@ async function shouldAddMoreTasks(taskCounter) {
|
|
|
104726
104896
|
}));
|
|
104727
104897
|
}
|
|
104728
104898
|
async function handleEstimationNormalization(tasks) {
|
|
104729
|
-
const totalEstimation = tasks
|
|
104899
|
+
const totalEstimation = totalTaskPercent(tasks);
|
|
104730
104900
|
if (totalEstimation === 100) {
|
|
104731
104901
|
output4.print(source_default.green("✓ Total estimation is 100%"));
|
|
104732
104902
|
return;
|
|
104733
104903
|
}
|
|
104734
104904
|
output4.print(source_default.yellow(`
|
|
104735
104905
|
Warning: Total estimation is ${totalEstimation}% (should be 100%)`));
|
|
104906
|
+
if (tasks.some((task) => copyCount(task) > 1)) {
|
|
104907
|
+
output4.print(source_default.yellow(" Each copy of a repeated task counts toward the total; adjust the percentages to reach 100%."));
|
|
104908
|
+
return;
|
|
104909
|
+
}
|
|
104736
104910
|
const shouldNormalize = await promptForNormalization();
|
|
104737
104911
|
if (shouldNormalize) {
|
|
104738
104912
|
normalizeEstimations(tasks);
|
|
@@ -104770,7 +104944,7 @@ async function editTasksInteractively(tasks, fieldSchemas, storyFieldSchemas) {
|
|
|
104770
104944
|
Tasks (${currentTasks.length}) — total: ${totalLabel}
|
|
104771
104945
|
`));
|
|
104772
104946
|
currentTasks.forEach((t3, i3) => {
|
|
104773
|
-
output4.print(source_default.gray(` ${i3 + 1}. ${t3.title} [${t3.estimationPercent ?? 0}%]`));
|
|
104947
|
+
output4.print(source_default.gray(` ${i3 + 1}. ${t3.title}${copyCount(t3) > 1 ? ` ×${copyCount(t3)}` : ""} [${t3.estimationPercent ?? 0}%]`));
|
|
104774
104948
|
});
|
|
104775
104949
|
output4.blankLine();
|
|
104776
104950
|
};
|
|
@@ -104792,7 +104966,7 @@ Tasks (${currentTasks.length}) — total: ${totalLabel}
|
|
|
104792
104966
|
const idx = assertNotCancelled(await select({
|
|
104793
104967
|
message: "Select task to edit:",
|
|
104794
104968
|
options: currentTasks.map((t3, i3) => ({
|
|
104795
|
-
label: `${i3 + 1}. ${t3.title} [${t3.estimationPercent ?? 0}%]`,
|
|
104969
|
+
label: `${i3 + 1}. ${t3.title}${copyCount(t3) > 1 ? ` ×${copyCount(t3)}` : ""} [${t3.estimationPercent ?? 0}%]`,
|
|
104796
104970
|
value: i3
|
|
104797
104971
|
}))
|
|
104798
104972
|
}));
|
|
@@ -104809,7 +104983,7 @@ Tasks (${currentTasks.length}) — total: ${totalLabel}
|
|
|
104809
104983
|
const idx = assertNotCancelled(await select({
|
|
104810
104984
|
message: "Select task to remove:",
|
|
104811
104985
|
options: currentTasks.map((t3, i3) => ({
|
|
104812
|
-
label: `${i3 + 1}. ${t3.title} [${t3.estimationPercent ?? 0}%]`,
|
|
104986
|
+
label: `${i3 + 1}. ${t3.title}${copyCount(t3) > 1 ? ` ×${copyCount(t3)}` : ""} [${t3.estimationPercent ?? 0}%]`,
|
|
104813
104987
|
value: i3
|
|
104814
104988
|
}))
|
|
104815
104989
|
}));
|
|
@@ -106509,6 +106683,7 @@ init_dist6();
|
|
|
106509
106683
|
init_logger2();
|
|
106510
106684
|
init_estimation_distribution();
|
|
106511
106685
|
init_custom_field_verifier();
|
|
106686
|
+
init_schema2();
|
|
106512
106687
|
init_template_library();
|
|
106513
106688
|
init_errors();
|
|
106514
106689
|
init_source();
|
|
@@ -106743,7 +106918,7 @@ function getTemplateSummary(template) {
|
|
|
106743
106918
|
const totalPercent = totalUnconditionalEstimationPercent(template.tasks);
|
|
106744
106919
|
return {
|
|
106745
106920
|
name: template.name,
|
|
106746
|
-
tasks: template.tasks
|
|
106921
|
+
tasks: generatedTaskCount(template.tasks),
|
|
106747
106922
|
totalEstimation: `${totalPercent}%`
|
|
106748
106923
|
};
|
|
106749
106924
|
}
|