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 title: Bootstrapping
type: epic type: epic
created: 2026-04-23T23:37:10.465324Z 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. 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 title: Milestones & Labels
type: epic type: epic
created: 2026-04-23T23:37:10.468947Z 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. 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 title: Output Polish
type: epic type: epic
created: 2026-04-23T23:37:10.472323Z 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. 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 title: Column Transitions
type: epic type: epic
created: 2026-04-23T23:37:10.476017Z 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. 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 title: Archiving
type: epic type: epic
created: 2026-04-23T23:37:10.479959Z 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. 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 title: Update stale documentation
type: task type: task
created: 2026-04-23T23:37:36.830655Z 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. 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 title: dew init command with module init hooks
type: story type: story
created: 2026-04-23T23:37:36.834286Z 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. 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 title: Add milestones and labels to Ticket model
type: story type: story
created: 2026-04-23T23:37:36.838770Z 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. 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 title: Grouped list view and ASCII board
type: story type: story
created: 2026-04-23T23:37:36.842362Z 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). 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 title: Enhanced search filters
type: story type: story
created: 2026-04-23T23:37:36.846212Z 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). 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 title: Column transition validation
type: story type: story
created: 2026-04-23T23:37:36.849741Z 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). 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" title: "Archive command: soft-delete tickets"
type: story type: story
created: 2026-04-23T23:37:36.853236Z 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. 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 title: Engineering Quality
type: epic type: epic
created: 2026-04-23T23:37:10.461762Z 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. 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" title: "Config refactor: DewConfig.raw + per-package extensions"
type: story type: story
created: 2026-04-23T23:37:36.822736Z 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. 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" title: "Ticket storage refactor: column subdirectories"
type: story type: story
created: 2026-04-23T23:37:36.826965Z 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. 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:path/path.dart' as p;
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
class TicketTypeConfig { /// Thin wrapper around the raw project YAML.
final String id; ///
final String name; /// Feature packages extend this class via Dart extension methods to expose
/// typed configuration (e.g. [KanbanDewConfig.kanban], [McpDewConfig.mcp]).
const TicketTypeConfig({required this.id, required this.name}); /// This keeps feature-specific config classes out of core.
}
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});
}
class DewConfig { class DewConfig {
final KanbanConfig kanban; final YamlMap raw;
final McpConfig mcp;
const DewConfig({required this.kanban, required this.mcp}); const DewConfig({required this.raw});
factory DewConfig.fromYaml(YamlMap yaml) { factory DewConfig.fromYaml(YamlMap yaml) => DewConfig(raw: 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,
);
}
} }
/// Locates the nearest project root and exposes the parsed [DewConfig]. /// Locates the nearest project root and exposes the parsed [DewConfig].

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import 'package:dew_core/dew_core.dart'; import 'package:dew_core/dew_core.dart';
import '../kanban_config.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../ticket_store.dart'; 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; library;
export 'src/dew_mcp_base.dart'; export 'src/dew_mcp_base.dart';
export 'src/mcp_config.dart';
import 'package:dew_core/dew_core.dart'; import 'package:dew_core/dew_core.dart';
import 'package:dew_mcp/src/dew_mcp_base.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: dew_core:
path: ../core path: ../core
dart_mcp: ^0.5.0 dart_mcp: ^0.5.0
yaml: ^3.1.0
dev_dependencies: dev_dependencies:
lints: ^6.0.0 lints: ^6.0.0