@lumpcode/recipes 0.0.13 → 0.0.15

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/index.js CHANGED
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import path$1 from 'node:path';
6
6
  import { fileURLToPath as fileURLToPath$1 } from 'node:url';
7
7
  import fs from 'fs/promises';
8
- import { dump, load } from 'js-yaml';
8
+ import { load, dump } from 'js-yaml';
9
9
 
10
10
  /** Run a shell script via `sh -c` (portable on Unix-like systems and Git Bash on Windows). */
11
11
  function shellCommand(script) {
@@ -223,22 +223,188 @@ function assertProjectRelativePath(filePath, label) {
223
223
  }
224
224
  function resolveBacklogPaths(configUrl, overrides) {
225
225
  const [lumpPath, lumpName] = lumpPathAndName(configUrl);
226
- const backlogFilePath = overrides?.backlogFilePath ?? path$1.join(lumpPath, 'BACKLOG.yml');
227
- const doneFilePath = overrides?.doneFilePath ?? path$1.join(lumpPath, 'DONE.yml');
228
- assertProjectRelativePath(backlogFilePath, 'backlogFilePath');
229
- assertProjectRelativePath(doneFilePath, 'doneFilePath');
226
+ const backlogItemsDir = overrides?.backlogItemsDir ?? path$1.join(lumpPath, 'backlogItems');
227
+ assertProjectRelativePath(backlogItemsDir, 'backlogItemsDir');
230
228
  return {
231
229
  lumpPath,
232
230
  lumpName,
233
- backlogFilePath,
234
- doneFilePath,
231
+ backlogItemsDir,
235
232
  };
236
233
  }
237
234
 
235
+ const CONTEXT_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
236
+ function assertRecord(value, location) {
237
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
238
+ throw new Error(`Backlog item ${location} must be an object`);
239
+ }
240
+ }
241
+ function assertStringField(record, field, location) {
242
+ const value = record[field];
243
+ if (typeof value !== 'string' || value.trim() === '') {
244
+ throw new Error(`Backlog item ${location} requires non-empty string field "${field}"`);
245
+ }
246
+ }
247
+ function assertNumberField(record, field, location) {
248
+ const value = record[field];
249
+ if (typeof value !== 'number' || Number.isNaN(value)) {
250
+ throw new Error(`Backlog item ${location} requires numeric field "${field}"`);
251
+ }
252
+ }
253
+ /** Validates and normalizes one backlog item; throws on invalid input. */
254
+ function validateBaseBacklogItem(raw, location) {
255
+ assertRecord(raw, location);
256
+ assertStringField(raw, 'name', location);
257
+ assertStringField(raw, 'task', location);
258
+ assertNumberField(raw, 'priority', location);
259
+ if (!CONTEXT_NAME_PATTERN.test(raw.name)) {
260
+ throw new Error(`Backlog item ${location} has invalid name "${raw.name}" (expected ^[a-zA-Z0-9_-]+$)`);
261
+ }
262
+ let dependsOn;
263
+ if (raw.dependsOn !== undefined) {
264
+ if (!Array.isArray(raw.dependsOn)) {
265
+ throw new Error(`Backlog item ${location} field "dependsOn" must be an array`);
266
+ }
267
+ for (const dep of raw.dependsOn) {
268
+ if (typeof dep !== 'string' || dep.trim() === '') {
269
+ throw new Error(`Backlog item ${location} field "dependsOn" must contain non-empty strings`);
270
+ }
271
+ }
272
+ dependsOn = raw.dependsOn;
273
+ }
274
+ return {
275
+ name: raw.name,
276
+ task: raw.task,
277
+ priority: raw.priority,
278
+ dependsOn,
279
+ };
280
+ }
281
+
282
+ async function listTodoFolderNames(todoDir) {
283
+ try {
284
+ const entries = await fs.readdir(todoDir, { withFileTypes: true });
285
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
286
+ }
287
+ catch (error) {
288
+ const err = error;
289
+ if (err.code === 'ENOENT') {
290
+ return [];
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+ function folderBacklogContexts({ backlogItemsDir, parseItem, parseContext, }) {
296
+ return async () => {
297
+ const todoDir = path$1.join(backlogItemsDir, 'todo');
298
+ const folderNames = await listTodoFolderNames(todoDir);
299
+ const discovered = await Promise.all(folderNames.map(async (folderName) => {
300
+ const descPath = path$1.join(todoDir, folderName, 'desc.yml');
301
+ let rawText;
302
+ try {
303
+ rawText = await fs.readFile(descPath, 'utf-8');
304
+ }
305
+ catch (error) {
306
+ const err = error;
307
+ if (err.code === 'ENOENT') {
308
+ throw new Error(`Backlog item folder "${folderName}" is missing desc.yml at ${descPath}`);
309
+ }
310
+ throw error;
311
+ }
312
+ const raw = load(rawText);
313
+ const baseItem = validateBaseBacklogItem(raw, `in folder "${folderName}"`);
314
+ if (baseItem.name !== folderName) {
315
+ throw new Error(`Backlog item folder "${folderName}" desc.yml name "${baseItem.name}" must match folder name`);
316
+ }
317
+ const item = parseItem ? parseItem(baseItem, folderName, raw) : baseItem;
318
+ return { item, folderName };
319
+ }));
320
+ discovered.sort((a, b) => {
321
+ if (a.item.priority !== b.item.priority) {
322
+ return a.item.priority - b.item.priority;
323
+ }
324
+ return a.item.name.localeCompare(b.item.name);
325
+ });
326
+ const allCtxs = await Promise.all(discovered.map(async ({ item, folderName }) => {
327
+ const { parsed, ignored } = parseContext
328
+ ? await parseContext(item, folderName)
329
+ : { parsed: undefined, ignored: false };
330
+ if (ignored) {
331
+ return null;
332
+ }
333
+ return {
334
+ name: item.name,
335
+ options: {
336
+ priority: item.priority,
337
+ dependsOnContexts: item.dependsOn,
338
+ },
339
+ variables: parsed?.variables ?? {},
340
+ ...parsed,
341
+ };
342
+ }));
343
+ return allCtxs.filter((ctx) => !!ctx);
344
+ };
345
+ }
346
+
347
+ function isPlainObject(value) {
348
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
349
+ }
350
+ /** Moves a finished backlog item folder from todo/ to completed/ after the context completes. */
351
+ const folderSetTaskDoneStep = (input) => {
352
+ const nameVarName = input.nameVarName ?? 'TASK_NAME';
353
+ return {
354
+ async commandFn({ context, workspacePath }) {
355
+ const variables = context.variables;
356
+ const itemsDirRelative = variables[input.itemsDirVarName];
357
+ const taskName = variables[nameVarName];
358
+ if (!itemsDirRelative || !taskName) {
359
+ throw new Error('Backlog items directory and task name are required');
360
+ }
361
+ const itemsDir = path$1.join(workspacePath, itemsDirRelative);
362
+ const fromDir = path$1.join(itemsDir, 'todo', taskName);
363
+ const toDir = path$1.join(itemsDir, 'completed', taskName);
364
+ const descPath = path$1.join(fromDir, 'desc.yml');
365
+ const completedDescPath = path$1.join(toDir, 'desc.yml');
366
+ if (!(await pathExists(fromDir))) {
367
+ return null;
368
+ }
369
+ if (await pathExists(toDir)) {
370
+ console.warn(`[lumpcode/recipes] Cannot move backlog item "${taskName}": already exists at ${toDir}`);
371
+ return null;
372
+ }
373
+ const rawText = await fs.readFile(descPath, 'utf-8');
374
+ const raw = load(rawText);
375
+ if (!isPlainObject(raw)) {
376
+ throw new Error(`Backlog desc.yml at ${descPath} must contain a YAML object`);
377
+ }
378
+ const updated = {
379
+ ...raw,
380
+ completedAt: new Date().toISOString(),
381
+ };
382
+ await fs.mkdir(path$1.join(itemsDir, 'completed'), { recursive: true });
383
+ await fs.rename(fromDir, toDir);
384
+ await fs.writeFile(completedDescPath, dump(updated));
385
+ return {
386
+ executable: 'cat',
387
+ args: [completedDescPath],
388
+ };
389
+ },
390
+ continueOnError: true,
391
+ };
392
+ };
393
+
394
+ let setTaskDoneDeprecatedWarned = false;
395
+ function warnSetTaskDoneDeprecated() {
396
+ if (setTaskDoneDeprecatedWarned) {
397
+ return;
398
+ }
399
+ setTaskDoneDeprecatedWarned = true;
400
+ console.warn('[lumpcode/recipes] setTaskDoneStep is deprecated; use folderSetTaskDoneStep. ' +
401
+ 'YAML backlog helpers will be removed in a future major version.');
402
+ }
238
403
  /** Moves a finished backlog item from BACKLOG.yml to DONE.yml after the context completes. */
239
404
  const setTaskDoneStep = (input) => {
240
405
  return {
241
406
  async commandFn({ context, workspacePath }) {
407
+ warnSetTaskDoneDeprecated();
242
408
  const variables = context.variables;
243
409
  const { backlogVarName, doneVarName } = input;
244
410
  const baseBacklogFilePath = variables[backlogVarName];
@@ -276,62 +442,25 @@ function resolveImplValidateCommand(implValidateCommand) {
276
442
  return (_input) => implValidateCommand;
277
443
  }
278
444
 
279
- const CONTEXT_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
280
- function assertRecord(value, index) {
281
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
282
- throw new Error(`Backlog item at index ${index} must be an object`);
283
- }
284
- }
285
- function assertStringField(record, field, index) {
286
- const value = record[field];
287
- if (typeof value !== 'string' || value.trim() === '') {
288
- throw new Error(`Backlog item at index ${index} requires non-empty string field "${field}"`);
289
- }
290
- }
291
- function assertNumberField(record, field, index) {
292
- const value = record[field];
293
- if (typeof value !== 'number' || Number.isNaN(value)) {
294
- throw new Error(`Backlog item at index ${index} requires numeric field "${field}"`);
445
+ let ymlBacklogDeprecatedWarned = false;
446
+ function warnYmlBacklogDeprecated() {
447
+ if (ymlBacklogDeprecatedWarned) {
448
+ return;
295
449
  }
450
+ ymlBacklogDeprecatedWarned = true;
451
+ console.warn('[lumpcode/recipes] ymlBacklogContexts is deprecated; use folderBacklogContexts. ' +
452
+ 'YAML backlog helpers will be removed in a future major version.');
296
453
  }
297
- /** Validates and normalizes one YAML backlog row; throws on invalid input. */
298
- function validateBaseBacklogItem(raw, index) {
299
- assertRecord(raw, index);
300
- assertStringField(raw, 'name', index);
301
- assertStringField(raw, 'task', index);
302
- assertNumberField(raw, 'priority', index);
303
- if (!CONTEXT_NAME_PATTERN.test(raw.name)) {
304
- throw new Error(`Backlog item at index ${index} has invalid name "${raw.name}" (expected ^[a-zA-Z0-9_-]+$)`);
305
- }
306
- let dependsOn;
307
- if (raw.dependsOn !== undefined) {
308
- if (!Array.isArray(raw.dependsOn)) {
309
- throw new Error(`Backlog item at index ${index} field "dependsOn" must be an array`);
310
- }
311
- for (const dep of raw.dependsOn) {
312
- if (typeof dep !== 'string' || dep.trim() === '') {
313
- throw new Error(`Backlog item at index ${index} field "dependsOn" must contain non-empty strings`);
314
- }
315
- }
316
- dependsOn = raw.dependsOn;
317
- }
318
- return {
319
- name: raw.name,
320
- task: raw.task,
321
- priority: raw.priority,
322
- dependsOn,
323
- };
324
- }
325
-
326
454
  function ymlBacklogContexts({ backlogFilePath, parseItem, parseContext, }) {
327
455
  return async () => {
456
+ warnYmlBacklogDeprecated();
328
457
  const raw = await fs.readFile(backlogFilePath, 'utf-8');
329
458
  const doc = load(raw);
330
459
  if (!Array.isArray(doc)) {
331
460
  throw new Error(`Backlog file ${backlogFilePath} must contain a YAML list`);
332
461
  }
333
462
  const allCtxs = await Promise.all(doc.map(async (rawItem, index) => {
334
- const baseItem = validateBaseBacklogItem(rawItem, index);
463
+ const baseItem = validateBaseBacklogItem(rawItem, `at index ${index}`);
335
464
  const item = parseItem ? parseItem(baseItem, index, rawItem) : baseItem;
336
465
  const { parsed, ignored } = parseContext
337
466
  ? await parseContext(item, index)
@@ -359,8 +488,8 @@ function defineRecipe(recipe) {
359
488
  const BACKLOG_STAGE_VAR = 'BACKLOG_STAGE';
360
489
  const BACKLOG_TASK_NAME_VAR = 'TASK_NAME';
361
490
  const BACKLOG_TASK_VAR = 'TASK';
362
- const BACKLOG_FILE_VAR = 'BACKLOG_FILE';
363
- const BACKLOG_DONE_FILE_VAR = 'DONE_FILE';
491
+ const BACKLOG_ITEMS_DIR_VAR = 'BACKLOG_ITEMS_DIR';
492
+ const BACKLOG_ITEM_DIR_VAR = 'BACKLOG_ITEM_DIR';
364
493
 
365
494
  function isIgnoredResolution(resolution) {
366
495
  return 'ignored' in resolution && resolution.ignored === true;
@@ -377,27 +506,25 @@ function buildStageSteps(stages, stageName) {
377
506
  if (stageDef.completion === 'moveToDone') {
378
507
  return [
379
508
  ...normalized,
380
- setTaskDoneStep({
381
- backlogVarName: BACKLOG_FILE_VAR,
382
- doneVarName: BACKLOG_DONE_FILE_VAR,
509
+ folderSetTaskDoneStep({
510
+ itemsDirVarName: BACKLOG_ITEMS_DIR_VAR,
383
511
  }),
384
512
  ];
385
513
  }
386
514
  return normalized;
387
515
  }
388
516
  function backlog(options) {
389
- const { configUrl, backlogFilePath: backlogFilePathOverride, doneFilePath: doneFilePathOverride, stages, parseItem, resolveItem, ...rest } = options;
517
+ const { configUrl, backlogItemsDir: backlogItemsDirOverride, stages, parseItem, resolveItem, ...rest } = options;
390
518
  const paths = resolveBacklogPaths(configUrl, {
391
- backlogFilePath: backlogFilePathOverride,
392
- doneFilePath: doneFilePathOverride,
519
+ backlogItemsDir: backlogItemsDirOverride,
393
520
  });
394
521
  const projectRoot = projectRootFromConfigUrl(configUrl);
395
- const absoluteBacklogPath = path$1.join(projectRoot, paths.backlogFilePath);
522
+ const absoluteBacklogItemsDir = path$1.join(projectRoot, paths.backlogItemsDir);
396
523
  return defineConfig({
397
- getContextListFn: ymlBacklogContexts({
398
- backlogFilePath: absoluteBacklogPath,
524
+ getContextListFn: folderBacklogContexts({
525
+ backlogItemsDir: absoluteBacklogItemsDir,
399
526
  parseItem,
400
- async parseContext(item) {
527
+ async parseContext(item, folderName) {
401
528
  const resolution = await resolveItem({ item, paths });
402
529
  if (isIgnoredResolution(resolution)) {
403
530
  return { ignored: true };
@@ -407,14 +534,15 @@ function backlog(options) {
407
534
  ...(item.dependsOn ?? []),
408
535
  ...(additionalDependsOnContexts ?? []),
409
536
  ];
537
+ const backlogItemDir = path$1.join(paths.backlogItemsDir, 'todo', folderName);
410
538
  return {
411
539
  parsed: {
412
540
  name: contextName ?? item.name,
413
541
  variables: {
414
542
  [BACKLOG_TASK_NAME_VAR]: item.name,
415
543
  [BACKLOG_TASK_VAR]: item.task,
416
- [BACKLOG_FILE_VAR]: paths.backlogFilePath,
417
- [BACKLOG_DONE_FILE_VAR]: paths.doneFilePath,
544
+ [BACKLOG_ITEMS_DIR_VAR]: paths.backlogItemsDir,
545
+ [BACKLOG_ITEM_DIR_VAR]: backlogItemDir,
418
546
  [BACKLOG_STAGE_VAR]: stage,
419
547
  ...variables,
420
548
  },
@@ -444,19 +572,19 @@ const backlogRecipe = defineRecipe((options) => backlog(options));
444
572
  const abstractionBacklog = defineRecipe((options) => {
445
573
  const { implValidateCommand, configUrl, implSteps, ...rest } = options;
446
574
  const projectRoot = projectRootFromConfigUrl(configUrl);
447
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
575
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
448
576
  return backlog({
449
577
  configUrl,
450
578
  async resolveItem({ item, paths }) {
451
- const itemPrdPath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
452
- const hasPrd = await pathExists(path$1.join(projectRoot, itemPrdPath));
453
- if (!hasPrd) {
579
+ const itemReqPath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'requirements.md');
580
+ const hasReq = await pathExists(path$1.join(projectRoot, itemReqPath));
581
+ if (!hasReq) {
454
582
  return { ignored: true };
455
583
  }
456
584
  return {
457
585
  stage: 'implementation',
458
586
  variables: {
459
- PRD_FILE: itemPrdPath,
587
+ REQ_FILE: itemReqPath,
460
588
  },
461
589
  };
462
590
  },
@@ -467,9 +595,9 @@ const abstractionBacklog = defineRecipe((options) => {
467
595
  steps: implSteps ?? [{
468
596
  promptFn({ context: ctx }) {
469
597
  const vars = ctx.variables;
470
- const { PRD_FILE, TASK_NAME, TASK } = vars;
598
+ const { REQ_FILE, TASK_NAME, TASK } = vars;
471
599
  return `
472
- Implement the abstraction described in @${PRD_FILE}.
600
+ Implement the abstraction described in @${REQ_FILE}.
473
601
 
474
602
  Backlog item: ${TASK_NAME}
475
603
  Task summary:
@@ -492,47 +620,48 @@ const abstractionBacklog = defineRecipe((options) => {
492
620
  });
493
621
 
494
622
  const abstractionFinder = defineRecipe((options) => {
495
- const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogFilePath, prdDirPath, doneFilePath, } = options;
623
+ const { maxPendingAbstractions = 5, scanDirectories, customPrompt, backlogItemsDir, } = options; // TODO : check if pending abstcations in backlog are less than maxPendingAbstractions
496
624
  return defineConfig({
497
625
  ...options,
498
626
  getContextListFn: ephemeralContextListFn({
499
627
  contextCount: maxPendingAbstractions,
500
628
  variables: {
501
- PRD_DIR_PATH: prdDirPath ?? '',
502
- BACKLOG_FILE_PATH: backlogFilePath ?? '',
503
- DONE_FILE_PATH: doneFilePath ?? '',
629
+ BACKLOG_ITEMS_DIR: backlogItemsDir,
504
630
  },
505
631
  }),
506
- steps: buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt }),
632
+ steps: buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt }),
507
633
  });
508
634
  });
509
- function buildFinderPrompt({ prdDirPath, backlogFilePath, doneFilePath, scanDirectories, customPrompt, }) {
635
+ function buildFinderPrompt({ backlogItemsDir, scanDirectories, customPrompt, }) {
510
636
  return customPrompt ? customPrompt() : `
511
637
  Scan ${(scanDirectories || []).map((dir) => `@${dir}`).join(' and ') || 'the codebase'} for duplicated logic that appears in multiple places (same pattern, not merely similar file structure).
512
638
 
513
- Read @${backlogFilePath} and @${doneFilePath}. Do not propose abstractions whose util name already appears in either file.
639
+ List existing backlog item names under @${backlogItemsDir}/todo/ and @${backlogItemsDir}/completed/. Do not propose abstractions whose util name already appears in either directory.
514
640
 
515
641
  Pick exactly one new abstraction that:
516
642
  - Has a clear util name matching ^[a-zA-Z0-9_-]+$ that describes the pattern it captures.
517
643
  - Would materialize as a new util under packages/apps/cli/src/utils/<utilName>/.
518
644
  - Would shrink the codebase: refactoring all call sites in packages/apps/cli should reduce net line count (excluding new unit tests).
519
645
 
520
- Add exactly one entry to @${backlogFilePath} with:
521
- - task: a concise summary of the repeated pattern, proposed util name, and affected areas
522
- - priority: max existing priority in BACKLOG.yml plus 1 (or 1 if the backlog is empty)
523
- - dependsOn: optional list of util names from BACKLOG.yml or DONE.yml that must land first (only when clearly needed)
524
-
525
- Write an implementation-ready PRD to @${prdDirPath}/<utilName>.prd.md for the same util name. The PRD should be self-contained and include:
526
- - Problem statement and repeated pattern
527
- - Goals and non-goals
528
- - Proposed util API and affected files
529
- - Acceptance criteria (including net line reduction and unit tests)
530
-
531
- Do not implement code. Only edit @${backlogFilePath} (append one item) and create the PRD file.
646
+ Create exactly one new backlog item folder at @${backlogItemsDir}/todo/<utilName>/ with:
647
+ - desc.yml containing:
648
+ - name: <utilName> (must match folder name)
649
+ - task: a concise summary of the repeated pattern, proposed util name, and affected areas
650
+ - priority: max existing priority in todo/ plus 1 (or 1 if todo/ is empty)
651
+ - dependsOn: optional list of util names from todo/ or completed/ that must land first (only when clearly needed)
652
+ - requirements.md: an implementation-ready requirements document for the same util name. It should be self-contained and include:
653
+ - Problem statement and repeated pattern
654
+ - Goals and non-goals
655
+ - Proposed util API and affected files
656
+ - Acceptance criteria (including net line reduction and unit tests)
657
+
658
+ Do not implement code. Only create @${backlogItemsDir}/todo/<utilName>/desc.yml and @${backlogItemsDir}/todo/<utilName>/requirements.md.
659
+
660
+ Do not take too much time looking for every possible abstraction. Once you found a good abstraction, stop and create the backlog item.
532
661
  `.trim();
533
662
  }
534
663
 
535
- const RESERVED_NAME_SUFFIXES = ['_prd', '_testPlan', '_tests_impl'];
664
+ const RESERVED_NAME_SUFFIXES = ['_req', '_testPlan', '_tests_impl'];
536
665
  function assertValidFeatureItemName(name) {
537
666
  for (const suffix of RESERVED_NAME_SUFFIXES) {
538
667
  if (name.endsWith(suffix)) {
@@ -542,8 +671,8 @@ function assertValidFeatureItemName(name) {
542
671
  }
543
672
  function featureContextName(itemName, stage) {
544
673
  switch (stage) {
545
- case 'makePrd':
546
- return `${itemName}_prd`;
674
+ case 'makeReq':
675
+ return `${itemName}_req`;
547
676
  case 'makeTestPlan':
548
677
  return `${itemName}_testPlan`;
549
678
  case 'testImpl':
@@ -557,17 +686,17 @@ function featureContextName(itemName, stage) {
557
686
  }
558
687
  }
559
688
  async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, baseBranch) {
560
- const prdFilePath = path$1.join(paths.lumpPath, 'prds', `${item.name}.prd.md`);
561
- const testPlanFilePath = path$1.join(paths.lumpPath, 'testPlans', `${item.name}.test.md`);
562
- const hasPrd = await pathExists(path$1.join(projectRoot, prdFilePath));
563
- if (!hasPrd) {
564
- if (item.manualPrd === true) {
689
+ const reqFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'requirements.md');
690
+ const testPlanFilePath = path$1.join(paths.backlogItemsDir, 'todo', item.name, 'testPlan.md');
691
+ const hasReq = await pathExists(path$1.join(projectRoot, reqFilePath));
692
+ if (!hasReq) {
693
+ if (item.manualReq === true) {
565
694
  return { ignored: true };
566
695
  }
567
696
  return {
568
- stage: 'makePrd',
569
- contextName: featureContextName(item.name, 'makePrd'),
570
- variables: { PRD_FILE: prdFilePath },
697
+ stage: 'makeReq',
698
+ contextName: featureContextName(item.name, 'makeReq'),
699
+ variables: { REQ_FILE: reqFilePath },
571
700
  };
572
701
  }
573
702
  const hasTestPlan = await pathExists(path$1.join(projectRoot, testPlanFilePath));
@@ -576,7 +705,7 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
576
705
  stage: 'makeTestPlan',
577
706
  contextName: featureContextName(item.name, 'makeTestPlan'),
578
707
  variables: {
579
- PRD_FILE: prdFilePath,
708
+ REQ_FILE: reqFilePath,
580
709
  TEST_PLAN_FILE: testPlanFilePath,
581
710
  },
582
711
  };
@@ -593,7 +722,7 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
593
722
  stage: 'implementation',
594
723
  contextName: featureContextName(item.name, 'implementation'),
595
724
  variables: {
596
- PRD_FILE: prdFilePath,
725
+ REQ_FILE: reqFilePath,
597
726
  TEST_PLAN_FILE: testPlanFilePath,
598
727
  },
599
728
  };
@@ -605,53 +734,52 @@ async function resolveFeatureBacklogItem(item, paths, projectRoot, lumpName, bas
605
734
  stage: 'testImpl',
606
735
  contextName: testsImplContextName,
607
736
  variables: {
608
- PRD_FILE: prdFilePath,
737
+ REQ_FILE: reqFilePath,
609
738
  TEST_PLAN_FILE: testPlanFilePath,
610
739
  },
611
740
  };
612
741
  }
613
742
  const featureBacklog = defineRecipe((options) => {
614
- const { configUrl, baseBranch, implValidateCommand, backlogFilePath, doneFilePath, ...rest } = options;
743
+ const { configUrl, baseBranch, implValidateCommand, backlogItemsDir, ...rest } = options;
615
744
  const projectRoot = projectRootFromConfigUrl(configUrl);
616
- const runImplValidation = resolveImplValidateCommand(implValidateCommand);
745
+ const runImplValidation = resolveImplValidateCommand(implValidateCommand ?? 'echo "No implementation validation command provided. I say, trust but verify, but well..."');
617
746
  return backlog({
618
747
  configUrl,
619
- backlogFilePath,
620
- doneFilePath,
748
+ backlogItemsDir,
621
749
  baseBranch,
622
- parseItem(baseItem, _index, raw) {
750
+ parseItem(baseItem, _folderName, raw) {
623
751
  assertValidFeatureItemName(baseItem.name);
624
752
  const record = raw;
625
- if (record.manualPrd !== undefined && typeof record.manualPrd !== 'boolean') {
626
- throw new Error(`Backlog item "${baseItem.name}" field "manualPrd" must be a boolean`);
753
+ if (record.manualReq !== undefined && typeof record.manualReq !== 'boolean') {
754
+ throw new Error(`Backlog item "${baseItem.name}" field "manualReq" must be a boolean`);
627
755
  }
628
756
  return {
629
757
  ...baseItem,
630
- manualPrd: record.manualPrd === true ? true : undefined,
758
+ manualReq: record.manualReq === true ? true : undefined,
631
759
  };
632
760
  },
633
761
  async resolveItem({ item, paths }) {
634
762
  return resolveFeatureBacklogItem(item, paths, projectRoot, paths.lumpName, baseBranch);
635
763
  },
636
764
  stages: {
637
- makePrd: {
765
+ makeReq: {
638
766
  completion: 'keepPending',
639
767
  steps: [
640
768
  {
641
769
  promptFn({ context: ctx }) {
642
770
  const vars = ctx.variables;
643
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE } = vars;
771
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE } = vars;
644
772
  return `
645
- Write a product requirements document (PRD) for the following Lumpcode backlog item from @${BACKLOG_FILE}.
773
+ Write a requirements document for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
646
774
 
647
775
  Task name: ${TASK_NAME}
648
776
 
649
777
  Task:
650
778
  ${TASK}
651
779
 
652
- Save the PRD to @${PRD_FILE}. Do not edit @${BACKLOG_FILE}.
780
+ Save the requirements document to @${REQ_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml.
653
781
 
654
- The PRD should be self-contained and implementation-ready. Include:
782
+ The requirements document should be self-contained and implementation-ready. Include:
655
783
  - Problem statement and motivation
656
784
  - Goals and non-goals
657
785
  - User stories / use cases
@@ -660,12 +788,12 @@ The PRD should be self-contained and implementation-ready. Include:
660
788
  - Technical approach and affected packages or docs
661
789
  - Acceptance criteria
662
790
 
663
- Do not implement the feature — only create the PRD markdown file.
664
- The PRD should not contain any testing strategy details.
791
+ Do not implement the feature — only create the requirements markdown file.
792
+ The requirements document should not contain any testing strategy details.
665
793
  `.trim();
666
794
  },
667
795
  },
668
- requireArtifactStep('PRD_FILE'),
796
+ requireArtifactStep('REQ_FILE'),
669
797
  ],
670
798
  },
671
799
  makeTestPlan: {
@@ -674,17 +802,17 @@ The PRD should not contain any testing strategy details.
674
802
  {
675
803
  promptFn({ context: ctx }) {
676
804
  const vars = ctx.variables;
677
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
805
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
678
806
  return `
679
- Write a test plan for the following Lumpcode backlog item from @${BACKLOG_FILE}.
807
+ Write a test plan for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
680
808
 
681
809
  Task name: ${TASK_NAME}
682
810
  Task:
683
811
  ${TASK}
684
812
 
685
- The PRD for this task is @${PRD_FILE}. The test plan should match the requirements of the PRD.
813
+ The requirements for this task are in @${REQ_FILE}. The test plan should match those requirements.
686
814
 
687
- Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_FILE} nor @${PRD_FILE}.
815
+ Save the test plan to @${TEST_PLAN_FILE}. Do not edit @${BACKLOG_ITEM_DIR}/desc.yml nor @${REQ_FILE}.
688
816
 
689
817
  The test plan should be self-contained and implementation-ready. Include:
690
818
  - Test cases
@@ -703,16 +831,16 @@ The test plan should be self-contained and implementation-ready. Include:
703
831
  {
704
832
  promptFn({ context: ctx }) {
705
833
  const vars = ctx.variables;
706
- const { BACKLOG_FILE, TASK_NAME, TASK, PRD_FILE, TEST_PLAN_FILE } = vars;
834
+ const { BACKLOG_ITEM_DIR, TASK_NAME, TASK, REQ_FILE, TEST_PLAN_FILE } = vars;
707
835
  return `
708
- Write a test implementation for the following Lumpcode backlog item from @${BACKLOG_FILE}.
836
+ Write a test implementation for the following backlog item from @${BACKLOG_ITEM_DIR}/desc.yml.
709
837
 
710
838
  Task name: ${TASK_NAME}
711
839
  Task:
712
840
  ${TASK}
713
841
 
714
842
  Follow the test plan in @${TEST_PLAN_FILE}.
715
- The PRD for this task is @${PRD_FILE}.
843
+ The requirements for this task are in @${REQ_FILE}.
716
844
  `.trim();
717
845
  },
718
846
  },
@@ -725,9 +853,9 @@ The PRD for this task is @${PRD_FILE}.
725
853
  {
726
854
  promptFn({ context: ctx }) {
727
855
  const vars = ctx.variables;
728
- const { PRD_FILE, TEST_PLAN_FILE } = vars;
856
+ const { REQ_FILE, TEST_PLAN_FILE } = vars;
729
857
  return `
730
- Implement the feature described in @${PRD_FILE}.
858
+ Implement the feature described in @${REQ_FILE}.
731
859
  The tests have already been implemented according to the test plan in @${TEST_PLAN_FILE}.
732
860
  The implementation should make the tests pass. Do not edit any test file.
733
861
  `.trim();
@@ -742,4 +870,4 @@ The implementation should make the tests pass. Do not edit any test file.
742
870
  });
743
871
  });
744
872
 
745
- export { BACKLOG_DONE_FILE_VAR, BACKLOG_FILE_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, getRecursiveSteps, lumpPathAndName, normalizeMaybePromGetter, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
873
+ export { BACKLOG_ITEMS_DIR_VAR, BACKLOG_ITEM_DIR_VAR, BACKLOG_STAGE_VAR, BACKLOG_TASK_NAME_VAR, BACKLOG_TASK_VAR, abstractionBacklog, abstractionFinder, backlog, backlogRecipe, defineRecipe, ephemeralContextListFn, featureBacklog, folderBacklogContexts, folderSetTaskDoneStep, getRecursiveSteps, lumpPathAndName, normalizeMaybePromGetter, projectRootFromConfigUrl, requireArtifactStep, resolveBacklogPaths, resolveFeatureBacklogItem, resolveImplValidateCommand, retryUntilGreen, setTaskDoneStep, shellCommand, validateBaseBacklogItem, ymlBacklogContexts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumpcode/recipes",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Lumpcode lump recipes and kit helpers",
5
5
  "keywords": [
6
6
  "lumpcode",