Config refactor: DewConfig.raw + per-package extensions (DEW-0007)

DewConfig in core is now a thin YamlMap wrapper. Feature-specific config
classes live in their own packages and expose themselves via Dart extensions:

- KanbanConfig, ColumnConfig, TicketTypeConfig + KanbanDewConfig extension
  moved to packages/kanban/lib/src/kanban_config.dart
- McpConfig + McpDewConfig extension added to
  packages/mcp/lib/src/mcp_config.dart
- DewConfig.fromYaml() now trivially wraps the raw YamlMap
- All call sites unchanged: context.config.kanban.* / context.config.mcp.*
- Added yaml dependency to dew_mcp pubspec
- Updated core test to validate raw yaml instead of typed fields
- Fixed cross-suite Directory.current isolation (existing issue, not introduced)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Chris Hendrickson 2026-04-23 19:54:35 -04:00
parent 951f0d8bc8
commit a25411d4fa
34 changed files with 150 additions and 86 deletions

View file

@ -3,6 +3,9 @@ id: DEW-0002
title: Bootstrapping
type: epic
created: 2026-04-23T23:37:10.465324Z
links:
- id: DEW-0010
type: parent_of
---
Make it easy to initialize a new Dew project. `dew init` scaffolds the .project/ directory and each module creates its own folders via init hooks.

View file

@ -3,6 +3,9 @@ id: DEW-0003
title: Milestones & Labels
type: epic
created: 2026-04-23T23:37:10.468947Z
links:
- id: DEW-0011
type: parent_of
---
Add freeform milestone and label fields to tickets for grouping and filtering. Both are List<String> in ticket frontmatter.

View file

@ -3,6 +3,11 @@ id: DEW-0004
title: Output Polish
type: epic
created: 2026-04-23T23:37:10.472323Z
links:
- id: DEW-0012
type: parent_of
- id: DEW-0013
type: parent_of
---
Improve human-facing CLI output. Grouped list by column, enhanced search filters, and an ASCII board view.

View file

@ -3,6 +3,9 @@ id: DEW-0005
title: Column Transitions
type: epic
created: 2026-04-23T23:37:10.476017Z
links:
- id: DEW-0014
type: parent_of
---
Optional config-driven validation of which column moves are allowed. E.g. prevent moving directly from todo to done.

View file

@ -3,6 +3,9 @@ id: DEW-0006
title: Archiving
type: epic
created: 2026-04-23T23:37:10.479959Z
links:
- id: DEW-0015
type: parent_of
---
Soft-delete tickets to an archive folder rather than permanently destroying them. Archived tickets are excluded from normal list/search but can be browsed.

View file

@ -3,6 +3,9 @@ id: DEW-0009
title: Update stale documentation
type: task
created: 2026-04-23T23:37:36.830655Z
links:
- id: DEW-0001
type: child_of
---
docs/mcp.md only lists 5 of 12 tools; update to reflect current tool set. docs/index.md references old interface names. docs/kanban.md is a stub; flesh it out. Update tool list, command reference, and architecture notes.

View file

@ -3,6 +3,9 @@ id: DEW-0010
title: dew init command with module init hooks
type: story
created: 2026-04-23T23:37:36.834286Z
links:
- id: DEW-0002
type: child_of
---
Add `dew init [path]` command. Core defines DewInitHook interface with onInit(root, options). Options include bool gitkeep (default true). Each package registers a hook. Kanban hook creates: column dirs (from config), archive/, attachments/ under .project/kanban/. --[no-]gitkeep flag adds .gitkeep to empty dirs. Must run after storage refactor so dir structure matches new layout.

View file

@ -3,6 +3,9 @@ id: DEW-0011
title: Add milestones and labels to Ticket model
type: story
created: 2026-04-23T23:37:36.838770Z
links:
- id: DEW-0003
type: child_of
---
Add milestones: List<String> and labels: List<String> to the Ticket model and YAML frontmatter. Support --milestone and --label flags on create/update. Add --label and --milestone filter flags to list and search commands. Depends on storage refactor.

View file

@ -3,6 +3,9 @@ id: DEW-0012
title: Grouped list view and ASCII board
type: story
created: 2026-04-23T23:37:36.842362Z
links:
- id: DEW-0004
type: child_of
---
dew kanban list groups output by column with counts. Add `dew kanban board` subcommand for a full ASCII kanban board view showing all columns and their tickets side by side (or stacked).

View file

@ -3,6 +3,9 @@ id: DEW-0013
title: Enhanced search filters
type: story
created: 2026-04-23T23:37:36.846212Z
links:
- id: DEW-0004
type: child_of
---
Add --column, --type, --label, --milestone filter flags to `dew kanban search`. Currently only matches on text. Filters should be combinable (AND semantics).

View file

@ -3,6 +3,9 @@ id: DEW-0014
title: Column transition validation
type: story
created: 2026-04-23T23:37:36.849741Z
links:
- id: DEW-0005
type: child_of
---
Add optional allowed_transitions map to column config in dew.yaml. When configured, MoveCommand validates the requested move is permitted and errors with a helpful message listing allowed next columns. Unconfigured = all moves allowed (current behaviour).

View file

@ -3,6 +3,9 @@ id: DEW-0015
title: "Archive command: soft-delete tickets"
type: story
created: 2026-04-23T23:37:36.853236Z
links:
- id: DEW-0006
type: child_of
---
Add `dew kanban archive --id <id>` that moves a ticket to .project/kanban/archive/ (and its attachments stay at attachments/<id>/). Archived tickets excluded from list/search by default; add --include-archived flag to opt in. Depends on storage refactor.

View file

@ -3,6 +3,13 @@ id: DEW-0001
title: Engineering Quality
type: epic
created: 2026-04-23T23:37:10.461762Z
links:
- id: DEW-0007
type: parent_of
- id: DEW-0008
type: parent_of
- id: DEW-0009
type: parent_of
---
Refactor and harden the core infrastructure before layering features on top. Covers config architecture, ticket storage model, and documentation accuracy.

View file

@ -3,6 +3,9 @@ id: DEW-0007
title: "Config refactor: DewConfig.raw + per-package extensions"
type: story
created: 2026-04-23T23:37:36.822736Z
links:
- id: DEW-0001
type: child_of
---
DewConfig in core becomes a thin wrapper: `class DewConfig { final YamlMap raw; }`. Each package adds a Dart extension with a typed getter (e.g. KanbanDewConfig, McpDewConfig). KanbanConfig/ColumnConfig/TicketTypeConfig move to dew_kanban; McpConfig moves to dew_mcp. Call sites unchanged: context.config.kanban.prefix etc.

View file

@ -3,6 +3,9 @@ id: DEW-0008
title: "Ticket storage refactor: column subdirectories"
type: story
created: 2026-04-23T23:37:36.826965Z
links:
- id: DEW-0001
type: child_of
---
Move ticket .md files from flat .project/kanban/ into column subdirectories (.project/kanban/todo/DEW-0001.md). Attachments live at .project/kanban/attachments/<ticket-id>/ (stable path, unaffected by moves). TicketStore changes: _filePath needs column, findById searches all column dirs, move physically moves the .md file, delete removes .md + attachments/<id>/ if present, create writes into correct column dir.

View file

@ -3,89 +3,17 @@ import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';
class TicketTypeConfig {
final String id;
final String name;
const TicketTypeConfig({required this.id, required this.name});
}
class ColumnConfig {
final String id;
final String name;
final String color;
const ColumnConfig({
required this.id,
required this.name,
required this.color,
});
}
class KanbanConfig {
final String prefix;
final List<TicketTypeConfig> ticketTypes;
final List<ColumnConfig> columns;
const KanbanConfig({
required this.prefix,
required this.ticketTypes,
required this.columns,
});
}
class McpConfig {
final String host;
final int port;
const McpConfig({required this.host, required this.port});
}
/// Thin wrapper around the raw project YAML.
///
/// Feature packages extend this class via Dart extension methods to expose
/// typed configuration (e.g. [KanbanDewConfig.kanban], [McpDewConfig.mcp]).
/// This keeps feature-specific config classes out of core.
class DewConfig {
final KanbanConfig kanban;
final McpConfig mcp;
final YamlMap raw;
const DewConfig({required this.kanban, required this.mcp});
const DewConfig({required this.raw});
factory DewConfig.fromYaml(YamlMap yaml) {
final dew = yaml['dew'] as YamlMap;
final mcpYaml = dew['mcp'] as YamlMap;
final mcp = McpConfig(
host: mcpYaml['host'] as String,
port: mcpYaml['port'] as int,
);
final kanbanYaml = dew['kanban'] as YamlMap;
final ticketTypes =
(kanbanYaml['ticket_types'] as YamlList)
.map(
(t) => TicketTypeConfig(
id: t['id'] as String,
name: t['name'] as String,
),
)
.toList();
final columns =
(kanbanYaml['columns'] as YamlList)
.map(
(c) => ColumnConfig(
id: c['id'] as String,
name: c['name'] as String,
color: c['color'] as String,
),
)
.toList();
return DewConfig(
kanban: KanbanConfig(
prefix: kanbanYaml['prefix'] as String,
ticketTypes: ticketTypes,
columns: columns,
),
mcp: mcp,
);
}
factory DewConfig.fromYaml(YamlMap yaml) => DewConfig(raw: yaml);
}
/// Locates the nearest project root and exposes the parsed [DewConfig].

View file

@ -69,13 +69,12 @@ dew:
await tempDir.delete(recursive: true);
});
test('find() loads config from .project/dew.yaml', () async {
test('find() loads config and exposes raw yaml', () async {
final ctx = await ProjectContext.find();
expect(ctx.config.kanban.prefix, 'TEST');
expect(ctx.config.kanban.ticketTypes, hasLength(1));
expect(ctx.config.kanban.columns.first.id, 'todo');
expect(ctx.config.mcp.host, 'localhost');
expect(ctx.config.mcp.port, 9090);
final dew = ctx.config.raw['dew'];
expect(dew['kanban']['prefix'], 'TEST');
expect(dew['mcp']['host'], 'localhost');
expect(dew['mcp']['port'], 9090);
});
test('find() locates config from a subdirectory', () async {

View file

@ -1,6 +1,7 @@
library;
export 'src/dew_kanban_base.dart';
export 'src/kanban_config.dart';
export 'src/ticket.dart';
export 'src/ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
class GetConfigCommand extends DewCommand with DewToolCommand {
@override

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p;
import '../ticket_store.dart';

View file

@ -0,0 +1,52 @@
import 'package:dew_core/dew_core.dart';
import 'package:yaml/yaml.dart';
class TicketTypeConfig {
final String id;
final String name;
const TicketTypeConfig({required this.id, required this.name});
}
class ColumnConfig {
final String id;
final String name;
final String color;
const ColumnConfig({required this.id, required this.name, required this.color});
}
class KanbanConfig {
final String prefix;
final List<TicketTypeConfig> ticketTypes;
final List<ColumnConfig> columns;
const KanbanConfig({
required this.prefix,
required this.ticketTypes,
required this.columns,
});
}
extension KanbanDewConfig on DewConfig {
KanbanConfig get kanban {
final kanbanYaml = (raw['dew'] as YamlMap)['kanban'] as YamlMap;
return KanbanConfig(
prefix: kanbanYaml['prefix'] as String,
ticketTypes:
(kanbanYaml['ticket_types'] as YamlList)
.map((t) => TicketTypeConfig(id: t['id'] as String, name: t['name'] as String))
.toList(),
columns:
(kanbanYaml['columns'] as YamlList)
.map(
(c) => ColumnConfig(
id: c['id'] as String,
name: c['name'] as String,
color: c['color'] as String,
),
)
.toList(),
);
}
}

View file

@ -1,6 +1,7 @@
library;
export 'src/dew_mcp_base.dart';
export 'src/mcp_config.dart';
import 'package:dew_core/dew_core.dart';
import 'package:dew_mcp/src/dew_mcp_base.dart';

View file

@ -0,0 +1,19 @@
import 'package:dew_core/dew_core.dart';
import 'package:yaml/yaml.dart';
class McpConfig {
final String host;
final int port;
const McpConfig({required this.host, required this.port});
}
extension McpDewConfig on DewConfig {
McpConfig get mcp {
final mcpYaml = (raw['dew'] as YamlMap)['mcp'] as YamlMap;
return McpConfig(
host: mcpYaml['host'] as String,
port: mcpYaml['port'] as int,
);
}
}

View file

@ -13,6 +13,7 @@ dependencies:
dew_core:
path: ../core
dart_mcp: ^0.5.0
yaml: ^3.1.0
dev_dependencies:
lints: ^6.0.0