Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ dist
.docusaurus
.coverage
test/validator.test.js
__mocks__

2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ All notable changes to this repository will be documented in this file.
Format and process

- The `Unreleased` section contains completed tasks that have been removed from `TODO.md` and are targeted for the next release.
- integration.test.task.003 — undefined — Test cleanup
- integration.test.task.002 — undefined — Integration test completion
- Each entry should include: task id, summary, author, PR or commit link, and a one-line description of the change.
- When creating a release, move the entries from `Unreleased` to a new versioned section (e.g. `## [1.0.0] - 2025-09-20`) and include release notes.

Expand Down
43 changes: 43 additions & 0 deletions __mocks__/chalk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const chalk = Object.assign((text) => text, {
green: (text) => text,
red: (text) => text,
yellow: (text) => text,
blue: (text) => text,
cyan: (text) => text,
magenta: (text) => text,
white: (text) => text,
black: (text) => text,
gray: (text) => text,
grey: (text) => text,
redBright: (text) => text,
greenBright: (text) => text,
yellowBright: (text) => text,
blueBright: (text) => text,
magentaBright: (text) => text,
cyanBright: (text) => text,
whiteBright: (text) => text,
bgRed: (text) => text,
bgGreen: (text) => text,
bgYellow: (text) => text,
bgBlue: (text) => text,
bgMagenta: (text) => text,
bgCyan: (text) => text,
bgWhite: (text) => text,
bgBlack: (text) => text,
bgRedBright: (text) => text,
bgGreenBright: (text) => text,
bgYellowBright: (text) => text,
bgBlueBright: (text) => text,
bgMagentaBright: (text) => text,
bgCyanBright: (text) => text,
bgWhiteBright: (text) => text,
bold: (text) => text,
dim: (text) => text,
italic: (text) => text,
underline: (text) => text,
inverse: (text) => text,
strikethrough: (text) => text,
reset: (text) => text,
});

module.exports = chalk;
5 changes: 5 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ module.exports = {
clearMocks: true,
restoreMocks: true,
resetMocks: true,
transformIgnorePatterns: ['node_modules/(?!chalk)'],
moduleNameMapper: {
'^chalk$': '<rootDir>/__mocks__/chalk.js',
},
forceExit: true,
};
42 changes: 42 additions & 0 deletions reference/standards/quality/code-quality.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Code Quality Standards

## Architectural Rules

### Index File Encapsulation

**NEVER** allow index files to export from outside their directory tree.

**❌ WRONG - Breaks encapsulation:**

```typescript
// src/types/rendering/index.ts
export * from './IRenderer';
export * from './OutputFormat';
export { ConsoleOutputWriter } from '../../core/rendering/console-output.writer'; // ❌ BAD
```

**✅ CORRECT - Maintains encapsulation:**

```typescript
// src/types/rendering/index.ts
export * from './IRenderer';
export * from './OutputFormat';

// Add exports to the appropriate domain index file instead:
// src/core/rendering/index.ts
export { ConsoleOutputWriter } from './console-output.writer';
```

**Rationale:**

- Maintains clear module boundaries and encapsulation
- Prevents circular dependencies
- Makes dependencies explicit and traceable
- Follows domain-driven design principles
- Improves maintainability and refactoring safety

**Enforcement:**

- Code reviews must flag any index file reaching outside its directory
- Automated linting rules should be added to prevent this pattern
- Refactoring should move cross-domain exports to appropriate domain boundaries
10 changes: 7 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import { Command } from 'commander';
import pino from 'pino';

import { ConsoleOutputWriter } from './core/rendering/console-output.writer';
import { getLogger } from './core/system/logger';
import { ObservabilityLogger } from './core/system/observability.logger';
import { CommandFactory } from './commands/shared/command.factory';
import { EXIT_CODES } from './constants/exit-codes';
import { EXIT_CODES } from './types/core';
import { CommandFactory } from './commands/command.factory';

/**
* Main CLI entry point for the Documentation-Driven Development toolkit.
Expand Down Expand Up @@ -36,8 +37,11 @@ const observabilityLogger = new ObservabilityLogger(pinoLogger);
const program = new Command();
program.name('dddctl').description('Documentation-Driven Development CLI').version('1.0.0');

// Create output writer for CLI messaging
const outputWriter = new ConsoleOutputWriter();

// Configure all commands through the factory
CommandFactory.configureProgram(program, baseLogger);
CommandFactory.configureProgram(program, baseLogger, outputWriter);

// Helper function to get safe command name
function getCommandName(): string {
Expand Down
127 changes: 127 additions & 0 deletions src/commands/add-task.command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Command } from 'commander';

import { TaskManager } from '../core/storage';
import { AddTaskArgs, CommandName, EXIT_CODES, ILogger, IOutputWriter } from '../types';

import { AddTaskCommand } from './add-task.command';

// Mock dependencies
jest.mock('../core/storage');
jest.mock('commander');

describe('AddTaskCommand', () => {
let logger: jest.Mocked<ILogger>;
let outputWriter: jest.Mocked<IOutputWriter>;
let command: AddTaskCommand;
let mockTaskManager: jest.Mocked<TaskManager>;

beforeEach(() => {
logger = {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
child: jest.fn(),
} as jest.Mocked<ILogger>;

outputWriter = {
info: jest.fn(),
error: jest.fn(),
success: jest.fn(),
warning: jest.fn(),
write: jest.fn(),
newline: jest.fn(),
writeFormatted: jest.fn(),
section: jest.fn(),
keyValue: jest.fn(),
} as jest.Mocked<IOutputWriter>;

mockTaskManager = {
addTaskFromFile: jest.fn(),
} as unknown as jest.Mocked<TaskManager>;

// Mock the TaskManager constructor
(TaskManager as jest.MockedClass<typeof TaskManager>).mockImplementation(() => mockTaskManager);

command = new AddTaskCommand(logger, outputWriter);
jest.clearAllMocks();
});

describe('execute', () => {
const args: AddTaskArgs = { file: 'test-task.md' };

it('should handle successful task addition', async () => {
mockTaskManager.addTaskFromFile.mockReturnValue(true);

await command.execute(args);

expect(mockTaskManager.addTaskFromFile).toHaveBeenCalledWith(args.file);
expect(outputWriter.success).toHaveBeenCalledWith(`Task added to TODO.md from ${args.file}`);
expect(logger.info).toHaveBeenCalledWith('Task added successfully', { file: args.file });
expect(process.exitCode).toBeUndefined();
});

it('should handle failed task addition', async () => {
mockTaskManager.addTaskFromFile.mockReturnValue(false);

await command.execute(args);

expect(mockTaskManager.addTaskFromFile).toHaveBeenCalledWith(args.file);
expect(logger.error).toHaveBeenCalledWith(`Failed to add task from ${args.file}`, {
file: args.file,
});
expect(process.exitCode).toBe(EXIT_CODES.GENERAL_ERROR);
});

it('should handle errors during task addition', async () => {
const error = new Error('File not found');
const mockImplementation = () => {
throw error;
};
mockTaskManager.addTaskFromFile.mockImplementation(mockImplementation);

await command.execute(args);

expect(mockTaskManager.addTaskFromFile).toHaveBeenCalledWith(args.file);
expect(logger.error).toHaveBeenCalledWith(`Error adding task: ${error.message}`, {
error: error.message,
file: args.file,
});
expect(process.exitCode).toBe(EXIT_CODES.NOT_FOUND);
});

it('should handle unknown errors during task addition', async () => {
const error = 'Unknown error';
const mockImplementation = () => {
throw error;
};
mockTaskManager.addTaskFromFile.mockImplementation(mockImplementation);

await command.execute(args);

expect(mockTaskManager.addTaskFromFile).toHaveBeenCalledWith(args.file);
expect(logger.error).toHaveBeenCalledWith(`Error adding task: ${error}`, {
error: error,
file: args.file,
});
expect(process.exitCode).toBe(EXIT_CODES.NOT_FOUND);
});
});

describe('configure', () => {
it('should configure the command with Commander.js', () => {
const parent = {
command: jest.fn().mockReturnValue({
argument: jest.fn().mockReturnThis(),
description: jest.fn().mockReturnThis(),
action: jest.fn().mockReturnThis(),
}),
} as unknown as Command;

AddTaskCommand.configure(parent, logger, outputWriter);

expect(parent.command).toHaveBeenCalledWith(CommandName.ADD);
// Additional assertions can be added if needed for argument and action setup
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import chalk from 'chalk';
import { Command } from 'commander';

import { ILogger } from '../../types/observability';
import { TaskManager } from '../../core/storage/task.manager';
import { AddTaskArgs } from '../../types/tasks';
import { EXIT_CODES } from '../../constants/exit-codes';
import { BaseCommand } from '../shared/base.command';
import { CommandName } from '../../types';
import { ConsoleOutputWriter } from '../core/rendering';
import { TaskManager } from '../core/storage';
import { AddTaskArgs, CommandName, EXIT_CODES, ILogger, IOutputWriter } from '../types';

import { BaseCommand } from './base.command';

/**
* Command for adding a new task from a file to the TODO.md.
Expand All @@ -26,6 +24,13 @@ export class AddTaskCommand extends BaseCommand {
readonly name = CommandName.ADD;
readonly description = 'Add a new task from a file';

constructor(
logger: ILogger,
protected override readonly outputWriter: IOutputWriter = new ConsoleOutputWriter(),
) {
super(logger, outputWriter);
}

/**
* Executes the add task command.
*
Expand All @@ -45,19 +50,47 @@ export class AddTaskCommand extends BaseCommand {
try {
const manager = new TaskManager(this.logger);
const added = manager.addTaskFromFile(args.file);

if (added) {
console.log(chalk.green(`Task added to TODO.md from ${args.file}`));
this.logger.info('Task added successfully', { file: args.file });
} else {
this.logger.error(`Failed to add task from ${args.file}`, { file: args.file });
process.exitCode = EXIT_CODES.GENERAL_ERROR;
this.handleSuccessfulAddition(args.file);
return Promise.resolve();
}

this.handleFailedAddition(args.file);
return Promise.resolve();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Error adding task: ${message}`, { error: message, file: args.file });
process.exitCode = EXIT_CODES.NOT_FOUND;
this.handleAdditionError(error, args.file);
return Promise.resolve();
}
return Promise.resolve();
}

/**
* Handles successful task addition.
* @param filePath - The path of the file being processed
*/
private handleSuccessfulAddition(filePath: string): void {
this.outputWriter.success(`Task added to TODO.md from ${filePath}`);
this.logger.info('Task added successfully', { file: filePath });
}

/**
* Handles failed task addition.
* @param filePath - The path of the file being processed
*/
private handleFailedAddition(filePath: string): void {
this.logger.error(`Failed to add task from ${filePath}`, { file: filePath });
process.exitCode = EXIT_CODES.GENERAL_ERROR;
}

/**
* Handles errors during task addition.
* @param error - The error that occurred
* @param filePath - The path of the file being processed
*/
private handleAdditionError(error: unknown, filePath: string): void {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Error adding task: ${message}`, { error: message, file: filePath });
process.exitCode = EXIT_CODES.NOT_FOUND;
}

/**
Expand All @@ -75,13 +108,13 @@ export class AddTaskCommand extends BaseCommand {
* AddTaskCommand.configure(program);
* ```
*/
static configure(parent: Command, logger: ILogger): void {
static configure(parent: Command, logger: ILogger, outputWriter?: IOutputWriter): void {
parent
.command(CommandName.ADD)
.argument('<file>', 'File containing the task to add')
.description('Add a new task from a file')
.action(async (file: string) => {
const cmd = new AddTaskCommand(logger);
const cmd = new AddTaskCommand(logger, outputWriter);
await cmd.execute({ file });
});
}
Expand Down
Loading