fix: resolve all analyzer info-level issues (cascade, deprecations, constructor ordering)

This commit is contained in:
UTO Manager Dev 2026-08-09 10:17:43 -04:00
parent 4af8543a29
commit 6afd2658a0
24 changed files with 410 additions and 386 deletions

View File

@ -1,11 +1,11 @@
# UTO Manager — Development Status # UTO Manager — Development Status
_Last updated: 2026-08-09 (fix/phase-4-analyzer-errors)_ _Last updated: 2026-08-09 (fix/phase-4-analyzer-round2)_
--- ---
## Current Branch ## Current Branch
`main` (fix/phase-4-analyzer-errors merged) `main` (fix/phase-4-analyzer-round2 merged)
## Overall Progress ## Overall Progress

View File

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uuid/uuid.dart';
import 'package:uto_manager/domain/entities/position_slot.dart' as domain; import 'package:uto_manager/domain/entities/position_slot.dart' as domain;
import 'package:uto_manager/domain/entities/unit_node.dart'; import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/repositories/unit_repository.dart'; import 'package:uto_manager/domain/repositories/unit_repository.dart';
@ -12,6 +11,7 @@ import 'package:uto_manager/infrastructure/codecs/unit_tree_codec.dart';
import 'package:uto_manager/infrastructure/database/app_database.dart'; import 'package:uto_manager/infrastructure/database/app_database.dart';
import 'package:uto_manager/infrastructure/database/daos/template_dao.dart'; import 'package:uto_manager/infrastructure/database/daos/template_dao.dart';
import 'package:uto_manager/infrastructure/repositories/drift_position_slot_repository.dart'; import 'package:uto_manager/infrastructure/repositories/drift_position_slot_repository.dart';
import 'package:uuid/uuid.dart';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// State // State

View File

@ -143,7 +143,7 @@ class UnitTreeCubit extends Cubit<UnitTreeState> {
final currentSelection = final currentSelection =
state is UnitTreeLoaded ? (state as UnitTreeLoaded).selectedUnitId : null; state is UnitTreeLoaded ? (state as UnitTreeLoaded).selectedUnitId : null;
emit(UnitTreeLoaded( emit(UnitTreeLoaded(
units: _units, slots: _slots, roots: roots, selectedUnitId: currentSelection)); units: _units, slots: _slots, roots: roots, selectedUnitId: currentSelection,),);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -232,14 +232,8 @@ class DeltaSynchronizer {
final CommandEventStore store; final CommandEventStore store;
/// Whether the synchronizer believes it has network connectivity. /// Whether the synchronizer believes it has network connectivity.
bool _isOnline = true; /// Set via [isOnline] setter; read via [isOnline] getter.
bool isOnline = true;
/// Sets the connectivity state. When set to true, [syncIfOnline] will
/// attempt to transmit pending events.
set isOnline(bool value) => _isOnline = value;
/// Returns true if the synchronizer believes it has network connectivity.
bool get isOnline => _isOnline;
/// Transmits all pending events if online. /// Transmits all pending events if online.
/// ///
@ -249,7 +243,7 @@ class DeltaSynchronizer {
/// In this implementation the transmission is simulated (no real network /// In this implementation the transmission is simulated (no real network
/// call). Replace [_transmit] with a real HTTP/gRPC call in production. /// call). Replace [_transmit] with a real HTTP/gRPC call in production.
Future<int> syncIfOnline() async { Future<int> syncIfOnline() async {
if (!_isOnline) return 0; if (!isOnline) return 0;
final pending = store.pendingEvents; final pending = store.pendingEvents;
if (pending.isEmpty) return 0; if (pending.isEmpty) return 0;

View File

@ -1,3 +1,5 @@
import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/services/template_instantiator.dart'; import 'package:uto_manager/domain/services/template_instantiator.dart';
/// Library of 2026 U.S. Army doctrinal force structure templates. /// Library of 2026 U.S. Army doctrinal force structure templates.
@ -169,11 +171,11 @@ abstract final class DoctrinalTemplates {
_riflePlatoon('1st Platoon'), _riflePlatoon('1st Platoon'),
_riflePlatoon('2nd Platoon'), _riflePlatoon('2nd Platoon'),
_riflePlatoon('3rd Platoon'), _riflePlatoon('3rd Platoon'),
UnitDefinition( const UnitDefinition(
name: 'Weapons Platoon', name: 'Weapons Platoon',
echelon: 'platoon', echelon: 'platoon',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Weapons Platoon Leader', positionTitle: 'Weapons Platoon Leader',
dutyMos: '11A', dutyMos: '11A',
@ -297,11 +299,11 @@ abstract final class DoctrinalTemplates {
_infantryBattalion('2-1 Mobile Infantry Battalion'), _infantryBattalion('2-1 Mobile Infantry Battalion'),
_infantryBattalion('3-1 Mobile Infantry Battalion'), _infantryBattalion('3-1 Mobile Infantry Battalion'),
// Cavalry Squadron // Cavalry Squadron
UnitDefinition( const UnitDefinition(
name: 'Cavalry Squadron', name: 'Cavalry Squadron',
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Squadron Commander', positionTitle: 'Squadron Commander',
dutyMos: '11A', dutyMos: '11A',
@ -320,7 +322,7 @@ abstract final class DoctrinalTemplates {
name: 'HHT', name: 'HHT',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'HHT Commander', positionTitle: 'HHT Commander',
dutyMos: '11A', dutyMos: '11A',
@ -339,7 +341,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Troop', name: 'Alpha Troop',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -358,7 +360,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Troop', name: 'Bravo Troop',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -377,7 +379,7 @@ abstract final class DoctrinalTemplates {
name: 'Anti-Tank Troop', name: 'Anti-Tank Troop',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -395,11 +397,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Field Artillery Battalion // Field Artillery Battalion
UnitDefinition( const UnitDefinition(
name: 'Field Artillery Battalion', name: 'Field Artillery Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '13A', dutyMos: '13A',
@ -423,7 +425,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Battery (105mm)', name: 'Alpha Battery (105mm)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -447,7 +449,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Battery (105mm)', name: 'Bravo Battery (105mm)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -466,7 +468,7 @@ abstract final class DoctrinalTemplates {
name: 'Charlie Battery (155mm Towed)', name: 'Charlie Battery (155mm Towed)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -484,11 +486,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Multi-Domain Effects Platoon // Multi-Domain Effects Platoon
UnitDefinition( const UnitDefinition(
name: 'Multi-Domain Effects Platoon (MDEP)', name: 'Multi-Domain Effects Platoon (MDEP)',
echelon: 'platoon', echelon: 'platoon',
branch: 'cyberEw', branch: 'cyberEw',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'MDEP Officer', positionTitle: 'MDEP Officer',
dutyMos: '17A', dutyMos: '17A',
@ -513,11 +515,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Hunter-Killer Platoon // Hunter-Killer Platoon
UnitDefinition( const UnitDefinition(
name: 'Hunter-Killer Platoon (HKP)', name: 'Hunter-Killer Platoon (HKP)',
echelon: 'platoon', echelon: 'platoon',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'HKP Leader', positionTitle: 'HKP Leader',
dutyMos: '11A', dutyMos: '11A',
@ -542,11 +544,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Brigade Engineer Battalion // Brigade Engineer Battalion
UnitDefinition( const UnitDefinition(
name: 'Brigade Engineer Battalion', name: 'Brigade Engineer Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'engineer', branch: 'engineer',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '12A', dutyMos: '12A',
@ -565,7 +567,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Engineer Company', name: 'Alpha Engineer Company',
echelon: 'company', echelon: 'company',
branch: 'engineer', branch: 'engineer',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '12A', dutyMos: '12A',
@ -588,11 +590,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Brigade Support Battalion // Brigade Support Battalion
UnitDefinition( const UnitDefinition(
name: 'Brigade Support Battalion', name: 'Brigade Support Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'logistics', branch: 'logistics',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '92A', dutyMos: '92A',
@ -669,11 +671,11 @@ abstract final class DoctrinalTemplates {
_infantryBattalion('2-2 Stryker Infantry Battalion'), _infantryBattalion('2-2 Stryker Infantry Battalion'),
_infantryBattalion('3-2 Stryker Infantry Battalion'), _infantryBattalion('3-2 Stryker Infantry Battalion'),
// Cavalry Reconnaissance Squadron // Cavalry Reconnaissance Squadron
UnitDefinition( const UnitDefinition(
name: 'Cavalry Reconnaissance Squadron', name: 'Cavalry Reconnaissance Squadron',
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Squadron Commander', positionTitle: 'Squadron Commander',
dutyMos: '11A', dutyMos: '11A',
@ -692,7 +694,7 @@ abstract final class DoctrinalTemplates {
name: 'HHT', name: 'HHT',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'HHT Commander', positionTitle: 'HHT Commander',
dutyMos: '11A', dutyMos: '11A',
@ -711,7 +713,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Cav Troop (Stryker)', name: 'Alpha Cav Troop (Stryker)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -730,7 +732,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Cav Troop (Stryker)', name: 'Bravo Cav Troop (Stryker)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -749,7 +751,7 @@ abstract final class DoctrinalTemplates {
name: 'Charlie Cav Troop (Stryker)', name: 'Charlie Cav Troop (Stryker)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -768,7 +770,7 @@ abstract final class DoctrinalTemplates {
name: 'Anti-Tank Troop (9× Stryker ATGM)', name: 'Anti-Tank Troop (9× Stryker ATGM)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -786,11 +788,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Field Artillery Battalion (M777A2) // Field Artillery Battalion (M777A2)
UnitDefinition( const UnitDefinition(
name: 'Field Artillery Battalion (M777A2)', name: 'Field Artillery Battalion (M777A2)',
echelon: 'battalion', echelon: 'battalion',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '13A', dutyMos: '13A',
@ -809,7 +811,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Battery (M777A2)', name: 'Alpha Battery (M777A2)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -828,7 +830,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Battery (M777A2)', name: 'Bravo Battery (M777A2)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -847,7 +849,7 @@ abstract final class DoctrinalTemplates {
name: 'Charlie Battery (M777A2)', name: 'Charlie Battery (M777A2)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -865,11 +867,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Brigade Engineer Battalion // Brigade Engineer Battalion
UnitDefinition( const UnitDefinition(
name: 'Brigade Engineer Battalion', name: 'Brigade Engineer Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'engineer', branch: 'engineer',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '12A', dutyMos: '12A',
@ -893,7 +895,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Engineer Company', name: 'Alpha Engineer Company',
echelon: 'company', echelon: 'company',
branch: 'engineer', branch: 'engineer',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '12A', dutyMos: '12A',
@ -916,11 +918,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Brigade Support Battalion // Brigade Support Battalion
UnitDefinition( const UnitDefinition(
name: 'Brigade Support Battalion', name: 'Brigade Support Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'logistics', branch: 'logistics',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '92A', dutyMos: '92A',
@ -984,11 +986,11 @@ abstract final class DoctrinalTemplates {
], ],
children: [ children: [
// Armor-Heavy CAB 1 // Armor-Heavy CAB 1
UnitDefinition( const UnitDefinition(
name: '1st Combined Arms Battalion (Armor-Heavy)', name: '1st Combined Arms Battalion (Armor-Heavy)',
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1017,7 +1019,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Tank Company (14× M1A2 SEPv3)', name: 'Alpha Tank Company (14× M1A2 SEPv3)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1036,7 +1038,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Tank Company (14× M1A2 SEPv3)', name: 'Bravo Tank Company (14× M1A2 SEPv3)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1055,7 +1057,7 @@ abstract final class DoctrinalTemplates {
name: 'Charlie Mech Infantry Company (14× M2A3 Bradley)', name: 'Charlie Mech Infantry Company (14× M2A3 Bradley)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1073,11 +1075,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Armor-Heavy CAB 2 // Armor-Heavy CAB 2
UnitDefinition( const UnitDefinition(
name: '2nd Combined Arms Battalion (Armor-Heavy)', name: '2nd Combined Arms Battalion (Armor-Heavy)',
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1101,7 +1103,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Tank Company (14× M1A2 SEPv3)', name: 'Alpha Tank Company (14× M1A2 SEPv3)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1120,7 +1122,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Tank Company (14× M1A2 SEPv3)', name: 'Bravo Tank Company (14× M1A2 SEPv3)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1139,7 +1141,7 @@ abstract final class DoctrinalTemplates {
name: 'Charlie Mech Infantry Company (14× M2A3 Bradley)', name: 'Charlie Mech Infantry Company (14× M2A3 Bradley)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1159,11 +1161,11 @@ abstract final class DoctrinalTemplates {
// Infantry-Heavy CAB // Infantry-Heavy CAB
_infantryBattalion('3rd Combined Arms Battalion (Infantry-Heavy)'), _infantryBattalion('3rd Combined Arms Battalion (Infantry-Heavy)'),
// Armored Cavalry Squadron // Armored Cavalry Squadron
UnitDefinition( const UnitDefinition(
name: 'Armored Cavalry Squadron', name: 'Armored Cavalry Squadron',
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Squadron Commander', positionTitle: 'Squadron Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1182,7 +1184,7 @@ abstract final class DoctrinalTemplates {
name: 'HHT', name: 'HHT',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'HHT Commander', positionTitle: 'HHT Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1201,7 +1203,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Armored Cav Troop', name: 'Alpha Armored Cav Troop',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1220,7 +1222,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Armored Cav Troop', name: 'Bravo Armored Cav Troop',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1239,7 +1241,7 @@ abstract final class DoctrinalTemplates {
name: 'Charlie Armored Cav Troop', name: 'Charlie Armored Cav Troop',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1258,7 +1260,7 @@ abstract final class DoctrinalTemplates {
name: 'Tank Troop (14× M1A2)', name: 'Tank Troop (14× M1A2)',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '11A', dutyMos: '11A',
@ -1276,11 +1278,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Field Artillery Battalion (M109A7 Paladin) // Field Artillery Battalion (M109A7 Paladin)
UnitDefinition( const UnitDefinition(
name: 'Field Artillery Battalion (M109A7 Paladin)', name: 'Field Artillery Battalion (M109A7 Paladin)',
echelon: 'battalion', echelon: 'battalion',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '13A', dutyMos: '13A',
@ -1299,7 +1301,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Battery (M109A7)', name: 'Alpha Battery (M109A7)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -1318,7 +1320,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Battery (M109A7)', name: 'Bravo Battery (M109A7)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -1337,7 +1339,7 @@ abstract final class DoctrinalTemplates {
name: 'Charlie Battery (M109A7)', name: 'Charlie Battery (M109A7)',
echelon: 'company', echelon: 'company',
branch: 'fires', branch: 'fires',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battery Commander', positionTitle: 'Battery Commander',
dutyMos: '13A', dutyMos: '13A',
@ -1355,11 +1357,11 @@ abstract final class DoctrinalTemplates {
], ],
), ),
// Brigade Engineer Battalion // Brigade Engineer Battalion
UnitDefinition( const UnitDefinition(
name: 'Brigade Engineer Battalion', name: 'Brigade Engineer Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'engineer', branch: 'engineer',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '12A', dutyMos: '12A',
@ -1378,7 +1380,7 @@ abstract final class DoctrinalTemplates {
name: 'Sapper Company', name: 'Sapper Company',
echelon: 'company', echelon: 'company',
branch: 'engineer', branch: 'engineer',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '12A', dutyMos: '12A',
@ -1408,11 +1410,11 @@ abstract final class DoctrinalTemplates {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Combat Aviation Brigade — Heavy (2026 doctrine). /// Combat Aviation Brigade — Heavy (2026 doctrine).
static final UnitDefinition cabHeavy = UnitDefinition( static const UnitDefinition cabHeavy = UnitDefinition(
name: 'Combat Aviation Brigade (Heavy)', name: 'Combat Aviation Brigade (Heavy)',
echelon: 'brigade', echelon: 'brigade',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Brigade Commander', positionTitle: 'Brigade Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1438,7 +1440,7 @@ abstract final class DoctrinalTemplates {
name: 'Attack Aviation Battalion (24× AH-64E Apache)', name: 'Attack Aviation Battalion (24× AH-64E Apache)',
echelon: 'battalion', echelon: 'battalion',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1467,7 +1469,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Attack Company', name: 'Alpha Attack Company',
echelon: 'company', echelon: 'company',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1486,7 +1488,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Attack Company', name: 'Bravo Attack Company',
echelon: 'company', echelon: 'company',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1508,7 +1510,7 @@ abstract final class DoctrinalTemplates {
name: 'Assault Helicopter Battalion (UH-60M / CH-47F)', name: 'Assault Helicopter Battalion (UH-60M / CH-47F)',
echelon: 'battalion', echelon: 'battalion',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1542,7 +1544,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Assault Company (UH-60M)', name: 'Alpha Assault Company (UH-60M)',
echelon: 'company', echelon: 'company',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1561,7 +1563,7 @@ abstract final class DoctrinalTemplates {
name: 'Bravo Heavy Lift Company (CH-47F)', name: 'Bravo Heavy Lift Company (CH-47F)',
echelon: 'company', echelon: 'company',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1583,7 +1585,7 @@ abstract final class DoctrinalTemplates {
name: 'Air Ambulance Company (15× HH-60M)', name: 'Air Ambulance Company (15× HH-60M)',
echelon: 'company', echelon: 'company',
branch: 'medical', branch: 'medical',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1618,7 +1620,7 @@ abstract final class DoctrinalTemplates {
name: 'Aviation Support Battalion', name: 'Aviation Support Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'logistics', branch: 'logistics',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '92A', dutyMos: '92A',
@ -1656,11 +1658,11 @@ abstract final class DoctrinalTemplates {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Combat Aviation Brigade — Light (2026 doctrine). /// Combat Aviation Brigade — Light (2026 doctrine).
static final UnitDefinition cabLight = UnitDefinition( static const UnitDefinition cabLight = UnitDefinition(
name: 'Combat Aviation Brigade (Light)', name: 'Combat Aviation Brigade (Light)',
echelon: 'brigade', echelon: 'brigade',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Brigade Commander', positionTitle: 'Brigade Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1686,7 +1688,7 @@ abstract final class DoctrinalTemplates {
name: 'Attack Reconnaissance Battalion (AH-64E + MQ-9A)', name: 'Attack Reconnaissance Battalion (AH-64E + MQ-9A)',
echelon: 'battalion', echelon: 'battalion',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1715,7 +1717,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Attack Company', name: 'Alpha Attack Company',
echelon: 'company', echelon: 'company',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1734,7 +1736,7 @@ abstract final class DoctrinalTemplates {
name: 'Troop F (MQ-9A / sUAS)', name: 'Troop F (MQ-9A / sUAS)',
echelon: 'company', echelon: 'company',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Troop Commander', positionTitle: 'Troop Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1761,7 +1763,7 @@ abstract final class DoctrinalTemplates {
name: 'Assault Helicopter Battalion (UH-60M / CH-47F)', name: 'Assault Helicopter Battalion (UH-60M / CH-47F)',
echelon: 'battalion', echelon: 'battalion',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1790,7 +1792,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha Assault Company', name: 'Alpha Assault Company',
echelon: 'company', echelon: 'company',
branch: 'aviation', branch: 'aviation',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1812,7 +1814,7 @@ abstract final class DoctrinalTemplates {
name: 'Assault Helicopter Battalion (Medical)', name: 'Assault Helicopter Battalion (Medical)',
echelon: 'battalion', echelon: 'battalion',
branch: 'medical', branch: 'medical',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1841,7 +1843,7 @@ abstract final class DoctrinalTemplates {
name: 'Alpha MEDEVAC Company', name: 'Alpha MEDEVAC Company',
echelon: 'company', echelon: 'company',
branch: 'medical', branch: 'medical',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Company Commander', positionTitle: 'Company Commander',
dutyMos: '153A', dutyMos: '153A',
@ -1868,7 +1870,7 @@ abstract final class DoctrinalTemplates {
name: 'Aviation Support Battalion', name: 'Aviation Support Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'logistics', branch: 'logistics',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Battalion Commander', positionTitle: 'Battalion Commander',
dutyMos: '92A', dutyMos: '92A',

View File

@ -90,12 +90,13 @@ class MtoeValidationEngine {
for (final unit in units) { for (final unit in units) {
final unitSlots = slotsByUnit[unit.unitId] ?? []; final unitSlots = slotsByUnit[unit.unitId] ?? [];
findings.addAll(_checkMosMismatches(unit, unitSlots, personnelById)); findings
findings.addAll(_checkRankMismatches(unit, unitSlots, personnelById)); ..addAll(_checkMosMismatches(unit, unitSlots, personnelById))
findings.addAll(_checkVacantKeyLeaders(unit, unitSlots)); ..addAll(_checkRankMismatches(unit, unitSlots, personnelById))
findings.addAll(_checkMissingCombatMedic(unit, unitSlots)); ..addAll(_checkVacantKeyLeaders(unit, unitSlots))
findings.addAll(_checkMissingFireSupport(unit, unitSlots)); ..addAll(_checkMissingCombatMedic(unit, unitSlots))
findings.addAll(_checkManning(unit, unitSlots)); ..addAll(_checkMissingFireSupport(unit, unitSlots))
..addAll(_checkManning(unit, unitSlots));
} }
findings.addAll(_checkStructuralHierarchy(units)); findings.addAll(_checkStructuralHierarchy(units));
@ -118,7 +119,8 @@ class MtoeValidationEngine {
final soldier = personnelById[slot.assignedPersonnelId]; final soldier = personnelById[slot.assignedPersonnelId];
if (soldier == null) continue; if (soldier == null) continue;
if (soldier.primaryMos != slot.dutyMos) { if (soldier.primaryMos != slot.dutyMos) {
findings.add(ValidationFinding( findings.add(
ValidationFinding(
severity: FindingSeverity.warning, severity: FindingSeverity.warning,
findingCode: 'V001', findingCode: 'V001',
description: '${unit.name} — "${slot.positionTitle}": assigned soldier ' description: '${unit.name} — "${slot.positionTitle}": assigned soldier '
@ -126,7 +128,8 @@ class MtoeValidationEngine {
'requires ${slot.dutyMos.code}.', 'requires ${slot.dutyMos.code}.',
unitId: unit.unitId, unitId: unit.unitId,
slotId: slot.slotId, slotId: slot.slotId,
)); ),
);
} }
} }
return findings; return findings;
@ -147,7 +150,8 @@ class MtoeValidationEngine {
final soldier = personnelById[slot.assignedPersonnelId]; final soldier = personnelById[slot.assignedPersonnelId];
if (soldier == null) continue; if (soldier == null) continue;
if (!soldier.payGrade.meetsMinimum(slot.authorizedRank)) { if (!soldier.payGrade.meetsMinimum(slot.authorizedRank)) {
findings.add(ValidationFinding( findings.add(
ValidationFinding(
severity: FindingSeverity.warning, severity: FindingSeverity.warning,
findingCode: 'V002', findingCode: 'V002',
description: '${unit.name} — "${slot.positionTitle}": assigned soldier ' description: '${unit.name} — "${slot.positionTitle}": assigned soldier '
@ -155,7 +159,8 @@ class MtoeValidationEngine {
'requires minimum ${slot.authorizedRank.payGrade}.', 'requires minimum ${slot.authorizedRank.payGrade}.',
unitId: unit.unitId, unitId: unit.unitId,
slotId: slot.slotId, slotId: slot.slotId,
)); ),
);
} }
} }
return findings; return findings;
@ -172,14 +177,16 @@ class MtoeValidationEngine {
final findings = <ValidationFinding>[]; final findings = <ValidationFinding>[];
for (final slot in slots) { for (final slot in slots) {
if (slot.isKeyLeader && slot.isVacant) { if (slot.isKeyLeader && slot.isVacant) {
findings.add(ValidationFinding( findings.add(
ValidationFinding(
severity: FindingSeverity.critical, severity: FindingSeverity.critical,
findingCode: 'V003', findingCode: 'V003',
description: '${unit.name} — Key leader position "${slot.positionTitle}" ' description: '${unit.name} — Key leader position "${slot.positionTitle}" '
'(${slot.dutyMos.code}/${slot.authorizedRank.payGrade}) is vacant.', '(${slot.dutyMos.code}/${slot.authorizedRank.payGrade}) is vacant.',
unitId: unit.unitId, unitId: unit.unitId,
slotId: slot.slotId, slotId: slot.slotId,
)); ),
);
} }
} }
return findings; return findings;
@ -249,7 +256,8 @@ class MtoeValidationEngine {
if (parent == null) continue; if (parent == null) continue;
if (!unit.echelon.isValidParent(parent.echelon)) { if (!unit.echelon.isValidParent(parent.echelon)) {
findings.add(ValidationFinding( findings.add(
ValidationFinding(
severity: FindingSeverity.critical, severity: FindingSeverity.critical,
findingCode: 'V006', findingCode: 'V006',
description: '"${unit.name}" (${unit.echelon.displayName}) is assigned under ' description: '"${unit.name}" (${unit.echelon.displayName}) is assigned under '
@ -257,7 +265,8 @@ class MtoeValidationEngine {
'A ${unit.echelon.displayName} must be subordinate to a ' 'A ${unit.echelon.displayName} must be subordinate to a '
'${_expectedParentEchelon(unit.echelon)?.displayName ?? "root"}.', '${_expectedParentEchelon(unit.echelon)?.displayName ?? "root"}.',
unitId: unit.unitId, unitId: unit.unitId,
)); ),
);
} }
} }
return findings; return findings;

View File

@ -13,12 +13,12 @@ class ValidationResult {
/// Human-readable reason if [isValid] is false. /// Human-readable reason if [isValid] is false.
final String? reason; final String? reason;
// ignore: sort_constructors_first
/// Creates a failing validation result with a [reason].
ValidationResult.invalid(this.reason) : isValid = false;
/// A passing validation result. /// A passing validation result.
static const valid = ValidationResult(isValid: true); static const valid = ValidationResult(isValid: true);
/// Creates a failing validation result with a [reason].
static ValidationResult invalid(String reason) =>
ValidationResult(isValid: false, reason: reason);
} }
/// Represents a node in the unit tree with its children and slots. /// Represents a node in the unit tree with its children and slots.
@ -213,7 +213,7 @@ class UnitTreeService {
/// Calculates the fill rate for a list of [slots] (0.0–1.0). /// Calculates the fill rate for a list of [slots] (0.0–1.0).
double calculateFillRate(List<PositionSlot> slots) { double calculateFillRate(List<PositionSlot> slots) {
if (slots.isEmpty) return 0.0; if (slots.isEmpty) return 0;
final filled = slots.where((s) => s.isFilled).length; final filled = slots.where((s) => s.isFilled).length;
return filled / slots.length; return filled / slots.length;
} }

View File

@ -43,9 +43,9 @@ class ValidationResponse {
/// Entry point for the background validation isolate. /// Entry point for the background validation isolate.
/// ///
/// Receives [ValidationRequest] messages on [receivePort], runs the /// Receives `ValidationRequest` messages on a `ReceivePort`, runs the
/// [MtoeValidationEngine], and sends a [ValidationResponse] back via the /// [MtoeValidationEngine], and sends a `ValidationResponse` back via the
/// request's [ValidationRequest.replyPort]. /// request's replyPort.
void validationIsolateEntryPoint(SendPort mainSendPort) { void validationIsolateEntryPoint(SendPort mainSendPort) {
final receivePort = ReceivePort(); final receivePort = ReceivePort();
// Send our receive port to the main isolate so it can send us requests. // Send our receive port to the main isolate so it can send us requests.

View File

@ -77,11 +77,11 @@ class PdfChartExporter {
const PdfChartExporter(); const PdfChartExporter();
static const String _classificationBanner = 'UNCLASSIFIED'; static const String _classificationBanner = 'UNCLASSIFIED';
static const double _margin = 36.0; // 0.5 inch in points static const double _margin = 36; // 0.5 inch in points
static const double _nodeW = 110.0; static const double _nodeW = 110;
static const double _nodeH = 36.0; static const double _nodeH = 36;
static const double _levelGap = 60.0; // vertical gap between echelon levels static const double _levelGap = 60; // vertical gap between echelon levels
static const double _siblingGap = 8.0; // horizontal gap between siblings static const double _siblingGap = 8; // horizontal gap between siblings
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Public API // Public API
@ -98,25 +98,25 @@ class PdfChartExporter {
DateTime? exportDate, DateTime? exportDate,
}) { }) {
final date = exportDate ?? DateTime.now(); final date = exportDate ?? DateTime.now();
final treeService = const UnitTreeService(); const treeService = UnitTreeService();
final roots = treeService.buildTree(units, slots); final roots = treeService.buildTree(units, slots);
// Compute layout positions for all nodes. // Compute layout positions for all nodes.
final positions = <String, _NodePos>{}; final positions = <String, _NodePos>{};
double xCursor = _margin; var xCursor = _margin;
for (final root in roots) { for (final root in roots) {
xCursor = _layoutSubtree(root, xCursor, 0, positions); xCursor = _layoutSubtree(root, xCursor, 0, positions);
xCursor += _siblingGap * 4; xCursor += _siblingGap * 4;
} }
// Build the PDF document. // Build the PDF document.
final builder = _PdfBuilder(pageSize: pageSize); final builder = _PdfBuilder(pageSize: pageSize)
builder.addCoverPage( ..addCoverPage(
unitDesignation: unitDesignation, unitDesignation: unitDesignation,
exportDate: date, exportDate: date,
classification: _classificationBanner, classification: _classificationBanner,
); )
builder.addChartPage( ..addChartPage(
positions: positions, positions: positions,
roots: roots, roots: roots,
units: units, units: units,
@ -148,7 +148,7 @@ class PdfChartExporter {
} }
// Layout children first to determine total width. // Layout children first to determine total width.
double childX = startX; var childX = startX;
for (final child in node.children) { for (final child in node.children) {
childX = _layoutSubtree(child, childX, depth + 1, positions); childX = _layoutSubtree(child, childX, depth + 1, positions);
} }
@ -207,29 +207,29 @@ class _PdfBuilder {
final h = pageSize.heightPt; final h = pageSize.heightPt;
final cx = w / 2; final cx = w / 2;
final stream = StringBuffer(); final stream = StringBuffer()
stream.writeln('BT'); ..writeln('BT')
// Classification banner — top // Classification banner — top
stream.writeln('/F1 10 Tf'); ..writeln('/F1 10 Tf')
stream.writeln('${cx - 40} ${h - 20} Td'); ..writeln('${cx - 40} ${h - 20} Td')
stream.writeln('($classification) Tj'); ..writeln('($classification) Tj')
// Title // Title
stream.writeln('/F1 24 Tf'); ..writeln('/F1 24 Tf')
stream.writeln('${cx - 80} ${h / 2 + 40} Td'); ..writeln('${cx - 80} ${h / 2 + 40} Td')
stream.writeln('(UTO Manager — Organizational Chart) Tj'); ..writeln('(UTO Manager — Organizational Chart) Tj')
// Unit designation // Unit designation
stream.writeln('/F1 18 Tf'); ..writeln('/F1 18 Tf')
stream.writeln('${cx - 60} ${h / 2} Td'); ..writeln('${cx - 60} ${h / 2} Td')
stream.writeln('($unitDesignation) Tj'); ..writeln('($unitDesignation) Tj')
// Export date // Export date
stream.writeln('/F1 12 Tf'); ..writeln('/F1 12 Tf')
stream.writeln('${cx - 40} ${h / 2 - 30} Td'); ..writeln('${cx - 40} ${h / 2 - 30} Td')
stream.writeln('(Exported: $dateStr) Tj'); ..writeln('(Exported: $dateStr) Tj')
// Classification banner — bottom // Classification banner — bottom
stream.writeln('/F1 10 Tf'); ..writeln('/F1 10 Tf')
stream.writeln('${cx - 40} 10 Td'); ..writeln('${cx - 40} 10 Td')
stream.writeln('($classification) Tj'); ..writeln('($classification) Tj')
stream.writeln('ET'); ..writeln('ET');
_addPage(stream.toString()); _addPage(stream.toString());
} }
@ -248,11 +248,10 @@ class _PdfBuilder {
final h = pageSize.heightPt; final h = pageSize.heightPt;
final cx = w / 2; final cx = w / 2;
final stream = StringBuffer(); final stream = StringBuffer()
// Classification banners // Classification banners
stream.writeln('BT /F1 10 Tf ${cx - 40} ${h - 20} Td ($classification) Tj ET'); ..writeln('BT /F1 10 Tf ${cx - 40} ${h - 20} Td ($classification) Tj ET')
stream.writeln('BT /F1 10 Tf ${cx - 40} 10 Td ($classification) Tj ET'); ..writeln('BT /F1 10 Tf ${cx - 40} 10 Td ($classification) Tj ET');
// Draw connector lines first (behind boxes). // Draw connector lines first (behind boxes).
for (final unit in units) { for (final unit in units) {
@ -266,9 +265,10 @@ class _PdfBuilder {
final parentCx = parentPos.x + PdfChartExporter._nodeW / 2; final parentCx = parentPos.x + PdfChartExporter._nodeW / 2;
final parentBottom = h - parentPos.y - PdfChartExporter._nodeH; final parentBottom = h - parentPos.y - PdfChartExporter._nodeH;
stream.writeln('0.4 0.4 0.4 RG'); // gray stroke stream
stream.writeln('0.5 w'); // line width ..writeln('0.4 0.4 0.4 RG') // gray stroke
stream.writeln('$parentCx $parentBottom m $childCx $childTop l S'); ..writeln('0.5 w') // line width
..writeln('$parentCx $parentBottom m $childCx $childTop l S');
} }
// Draw node boxes and labels. // Draw node boxes and labels.
@ -281,21 +281,21 @@ class _PdfBuilder {
// Box fill color based on echelon. // Box fill color based on echelon.
final rgb = _echelonColor(unit.echelon); final rgb = _echelonColor(unit.echelon);
stream.writeln('${rgb[0]} ${rgb[1]} ${rgb[2]} rg'); // fill
stream.writeln('0.2 0.2 0.4 RG'); // stroke
stream.writeln('0.8 w');
stream.writeln(
'$x $y ${PdfChartExporter._nodeW} ${PdfChartExporter._nodeH} re B',
);
// Unit name label (truncated to fit box). // Unit name label (truncated to fit box).
final label = _truncate(unit.name, 18); final label = _truncate(unit.name, 18);
final echelonLabel = unit.echelon.displayName; final echelonLabel = unit.echelon.displayName;
stream.writeln('1 1 1 rg'); // white text stream
stream.writeln( ..writeln('${rgb[0]} ${rgb[1]} ${rgb[2]} rg') // fill
..writeln('0.2 0.2 0.4 RG') // stroke
..writeln('0.8 w')
..writeln(
'$x $y ${PdfChartExporter._nodeW} ${PdfChartExporter._nodeH} re B',
)
..writeln('1 1 1 rg') // white text
..writeln(
'BT /F1 7 Tf ${x + 4} ${y + PdfChartExporter._nodeH - 12} Td ($label) Tj ET', 'BT /F1 7 Tf ${x + 4} ${y + PdfChartExporter._nodeH - 12} Td ($label) Tj ET',
); )
stream.writeln( ..writeln(
'BT /F1 6 Tf ${x + 4} ${y + 4} Td ($echelonLabel) Tj ET', 'BT /F1 6 Tf ${x + 4} ${y + 4} Td ($echelonLabel) Tj ET',
); );
} }
@ -385,9 +385,9 @@ class _PdfBuilder {
// Ensure font object exists. // Ensure font object exists.
_fontObjNum(); _fontObjNum();
final buf = StringBuffer(); final buf = StringBuffer()
buf.writeln('%PDF-1.4'); ..writeln('%PDF-1.4')
buf.writeln('%âãÏÓ'); // binary comment to mark as binary file ..writeln('%âãÏÓ'); // binary comment to mark as binary file
// Write all objects and record their byte offsets for the xref table. // Write all objects and record their byte offsets for the xref table.
final offsets = <int>[]; final offsets = <int>[];
@ -421,19 +421,21 @@ class _PdfBuilder {
// Cross-reference table. // Cross-reference table.
final xrefOffset = byteOffset; final xrefOffset = byteOffset;
final totalObjects = offsets.length; final totalObjects = offsets.length;
buf.writeln('xref'); buf
buf.writeln('0 ${totalObjects + 1}'); ..writeln('xref')
buf.writeln('0000000000 65535 f '); ..writeln('0 ${totalObjects + 1}')
..writeln('0000000000 65535 f ');
for (final offset in offsets) { for (final offset in offsets) {
buf.writeln(offset.toString().padLeft(10, '0') + ' 00000 n '); buf.writeln('${offset.toString().padLeft(10, '0')} 00000 n ');
} }
// Trailer. // Trailer.
buf.writeln('trailer'); buf
buf.writeln('<< /Size ${totalObjects + 1} /Root $catalogObjNum 0 R >>'); ..writeln('trailer')
buf.writeln('startxref'); ..writeln('<< /Size ${totalObjects + 1} /Root $catalogObjNum 0 R >>')
buf.writeln(xrefOffset); ..writeln('startxref')
buf.writeln('%%EOF'); ..writeln(xrefOffset)
..writeln('%%EOF');
return Uint8List.fromList(buf.toString().codeUnits); return Uint8List.fromList(buf.toString().codeUnits);
} }

View File

@ -17,7 +17,7 @@ import 'package:uto_manager/infrastructure/codecs/unit_tree_codec.dart';
/// significantly smaller than the equivalent JSON export. The encoding /// significantly smaller than the equivalent JSON export. The encoding
/// uses a length-prefixed binary format: /// uses a length-prefixed binary format:
/// ///
/// ``` /// ```text
/// [4 bytes: magic] [4 bytes: version] [4 bytes: payload length] /// [4 bytes: magic] [4 bytes: version] [4 bytes: payload length]
/// [N bytes: gzip-compressed JSON payload] /// [N bytes: gzip-compressed JSON payload]
/// ``` /// ```
@ -69,11 +69,10 @@ class ProtobufCodec {
final payloadLength = compressed.length; final payloadLength = compressed.length;
final totalLength = 12 + payloadLength; final totalLength = 12 + payloadLength;
final result = Uint8List(totalLength); final result = Uint8List(totalLength);
final header = ByteData.sublistView(result, 0, 12); ByteData.sublistView(result, 0, 12)
..setUint32(0, _magic)
header.setUint32(0, _magic, Endian.big); ..setUint32(4, _version)
header.setUint32(4, _version, Endian.big); ..setUint32(8, payloadLength);
header.setUint32(8, payloadLength, Endian.big);
result.setRange(12, totalLength, compressed); result.setRange(12, totalLength, compressed);
return result; return result;
@ -95,9 +94,9 @@ class ProtobufCodec {
} }
final header = ByteData.sublistView(bytes, 0, 12); final header = ByteData.sublistView(bytes, 0, 12);
final magic = header.getUint32(0, Endian.big); final magic = header.getUint32(0);
final version = header.getUint32(4, Endian.big); final version = header.getUint32(4);
final payloadLength = header.getUint32(8, Endian.big); final payloadLength = header.getUint32(8);
if (magic != _magic) { if (magic != _magic) {
throw FormatException( throw FormatException(
@ -137,7 +136,7 @@ class ProtobufCodec {
/// A ratio < 1.0 means the binary encoding is smaller than the JSON. /// A ratio < 1.0 means the binary encoding is smaller than the JSON.
/// Typical BCT trees achieve 0.25–0.40 (60–75% size reduction). /// Typical BCT trees achieve 0.25–0.40 (60–75% size reduction).
static double compressionRatio(int binaryBytes, int jsonBytes) { static double compressionRatio(int binaryBytes, int jsonBytes) {
if (jsonBytes == 0) return 1.0; if (jsonBytes == 0) return 1;
return binaryBytes / jsonBytes; return binaryBytes / jsonBytes;
} }
} }

View File

@ -428,7 +428,11 @@ class HomeScreen extends StatelessWidget {
final a = valState.advisories.length; final a = valState.advisories.length;
if (valState.isClean) { if (valState.isClean) {
return _statusChip( return _statusChip(
theme, Icons.check_circle_outline, 'MTOE Valid', Colors.green.shade400); theme,
Icons.check_circle_outline,
'MTOE Valid',
Colors.green.shade400,
);
} }
return Row( return Row(
children: [ children: [
@ -437,8 +441,12 @@ class HomeScreen extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
if (w > 0) ...[ if (w > 0) ...[
_statusChip(theme, Icons.warning_amber_outlined, '$w warnings', _statusChip(
Colors.orange.shade400), theme,
Icons.warning_amber_outlined,
'$w warnings',
Colors.orange.shade400,
),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
if (a > 0) if (a > 0)
@ -572,14 +580,20 @@ class _ExportDialogState extends State<_ExportDialog> {
children: [ children: [
const Text('Format:', style: TextStyle(fontWeight: FontWeight.bold)), const Text('Format:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8), const SizedBox(height: 8),
..._ExportFormat.values.map( RadioGroup<_ExportFormat>(
groupValue: _format,
onChanged: (v) => setState(() => _format = v!),
child: Column(
children: _ExportFormat.values
.map(
(f) => RadioListTile<_ExportFormat>( (f) => RadioListTile<_ExportFormat>(
title: Text(f.label), title: Text(f.label),
subtitle: Text(f.description, style: const TextStyle(fontSize: 11)), subtitle: Text(f.description, style: const TextStyle(fontSize: 11)),
value: f, value: f,
groupValue: _format,
dense: true, dense: true,
onChanged: (v) => setState(() => _format = v!), ),
)
.toList(),
), ),
), ),
if (_format == _ExportFormat.pdf) ...[ if (_format == _ExportFormat.pdf) ...[
@ -653,31 +667,37 @@ class _ExportDialogState extends State<_ExportDialog> {
switch (_format) { switch (_format) {
case _ExportFormat.json: case _ExportFormat.json:
final codec = const UnitTreeCodec(); const codec = UnitTreeCodec();
final json = codec.encode(units: units, slots: slots, personnel: const []); final json = codec.encode(units: units, slots: slots, personnel: const []);
setState(() => _result = 'JSON export ready — ${json.length} characters ' setState(
'(${(json.length / 1024).toStringAsFixed(1)} KB)'); () => _result = 'JSON export ready — ${json.length} characters '
'(${(json.length / 1024).toStringAsFixed(1)} KB)',
);
case _ExportFormat.binary: case _ExportFormat.binary:
final codec = const ProtobufCodec(); const codec = ProtobufCodec();
final bytes = codec.encode(units: units, slots: slots, personnel: const []); final bytes = codec.encode(units: units, slots: slots, personnel: const []);
final jsonLen = final jsonLen =
const UnitTreeCodec().encode(units: units, slots: slots, personnel: const []).length; const UnitTreeCodec().encode(units: units, slots: slots, personnel: const []).length;
final ratio = ProtobufCodec.compressionRatio(bytes.length, jsonLen); final ratio = ProtobufCodec.compressionRatio(bytes.length, jsonLen);
setState(() => _result = 'Binary export ready — ${bytes.length} bytes ' setState(
'(${(ratio * 100).round()}% of JSON size)'); () => _result = 'Binary export ready — ${bytes.length} bytes '
'(${(ratio * 100).round()}% of JSON size)',
);
case _ExportFormat.pdf: case _ExportFormat.pdf:
final exporter = const PdfChartExporter(); const exporter = PdfChartExporter();
final bytes = exporter.export( final bytes = exporter.export(
units: units, units: units,
slots: slots, slots: slots,
unitDesignation: units.isNotEmpty ? units.first.name : 'Unit Tree', unitDesignation: units.isNotEmpty ? units.first.name : 'Unit Tree',
pageSize: _pageSize, pageSize: _pageSize,
); );
setState(() => _result = 'PDF export ready — ${bytes.length} bytes ' setState(
() => _result = 'PDF export ready — ${bytes.length} bytes '
'(${(bytes.length / 1024).toStringAsFixed(1)} KB), ' '(${(bytes.length / 1024).toStringAsFixed(1)} KB), '
'${_pageSize.displayName}'); '${_pageSize.displayName}',
);
} }
} catch (e) { } catch (e) {
setState(() => _error = 'Export failed: $e'); setState(() => _error = 'Export failed: $e');

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:uto_manager/domain/entities/personnel_record.dart'; import 'package:uto_manager/domain/entities/personnel_record.dart';
import 'package:uto_manager/presentation/widgets/position_slot_card.dart' show PositionSlotCard;
/// A draggable list tile representing a soldier in the personnel panel. /// A draggable list tile representing a soldier in the personnel panel.
/// ///
/// Can be dragged onto a [PositionSlotCard] to assign the soldier. /// Can be dragged onto a [PositionSlotCard] to assign the soldier.
class PersonnelListTile extends StatelessWidget { class PersonnelListTile extends StatelessWidget {
const PersonnelListTile({ const PersonnelListTile({
super.key, required this.record, super.key,
required this.record,
this.onTap, this.onTap,
}); });

View File

@ -8,8 +8,8 @@ import 'package:uto_manager/domain/entities/position_slot.dart';
/// Supports drag-and-drop target for soldier assignment. /// Supports drag-and-drop target for soldier assignment.
class PositionSlotCard extends StatelessWidget { class PositionSlotCard extends StatelessWidget {
const PositionSlotCard({ const PositionSlotCard({
super.key,
required this.slot, required this.slot,
super.key,
this.assignedSoldier, this.assignedSoldier,
this.onUnassign, this.onUnassign,
this.onAcceptDrop, this.onAcceptDrop,
@ -121,7 +121,9 @@ class PositionSlotCard extends StatelessWidget {
return const Color(0xFFFF9800); return const Color(0xFFFF9800);
case ReadinessState.notMissionCapable: case ReadinessState.notMissionCapable:
return const Color(0xFFF44336); return const Color(0xFFF44336);
default: case ReadinessState.unknown:
return Colors.grey;
case null:
return Colors.grey; return Colors.grey;
} }
} }

View File

@ -7,10 +7,7 @@ import 'package:uto_manager/domain/services/unit_tree_service.dart';
/// Shows the unit's echelon, branch, name, and fill rate indicator. /// Shows the unit's echelon, branch, name, and fill rate indicator.
class UnitNodeCard extends StatelessWidget { class UnitNodeCard extends StatelessWidget {
const UnitNodeCard({ const UnitNodeCard({
super.key, required this.treeNode, required this.isSelected, required this.onTap, super.key,
required this.treeNode,
required this.isSelected,
required this.onTap,
}); });
final UnitTreeNode treeNode; final UnitTreeNode treeNode;

View File

@ -8,11 +8,7 @@ import 'package:uto_manager/presentation/widgets/unit_node_card.dart';
/// [UnitNodeCard] widgets arranged in a hierarchical tree layout. /// [UnitNodeCard] widgets arranged in a hierarchical tree layout.
class UtoCanvasWidget extends StatefulWidget { class UtoCanvasWidget extends StatefulWidget {
const UtoCanvasWidget({ const UtoCanvasWidget({
super.key, required this.roots, required this.selectedUnitId, required this.onUnitSelected, required this.onReparent, super.key,
required this.roots,
required this.selectedUnitId,
required this.onUnitSelected,
required this.onReparent,
}); });
/// Root-level tree nodes to render. /// Root-level tree nodes to render.
@ -49,12 +45,11 @@ class _UtoCanvasWidgetState extends State<UtoCanvasWidget> {
return InteractiveViewer( return InteractiveViewer(
transformationController: _transformController, transformationController: _transformController,
minScale: 0.2, minScale: 0.2,
maxScale: 3.0, maxScale: 3,
boundaryMargin: const EdgeInsets.all(500), boundaryMargin: const EdgeInsets.all(500),
child: Padding( child: Padding(
padding: const EdgeInsets.all(40), padding: const EdgeInsets.all(40),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: widget.roots.map((root) => _buildSubtree(root, 0)).toList(), children: widget.roots.map((root) => _buildSubtree(root, 0)).toList(),
), ),
), ),

View File

@ -81,7 +81,7 @@ void main() {
test('MOS mismatch is flagged but does not block assignment', () { test('MOS mismatch is flagged but does not block assignment', () {
final slot = makeSlot(mosCode: '68W'); final slot = makeSlot(mosCode: '68W');
final soldier = makeSoldier(mosCode: '11B'); final soldier = makeSoldier();
final outcome = service.assign(slot, soldier); final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isTrue); expect(outcome.isSuccess, isTrue);
expect(outcome.hasMosMismatch, isTrue); expect(outcome.hasMosMismatch, isTrue);
@ -96,7 +96,7 @@ void main() {
}); });
test('no rank mismatch when soldier meets minimum rank', () { test('no rank mismatch when soldier meets minimum rank', () {
final slot = makeSlot(rank: AuthorizedRank.e5); final slot = makeSlot();
final soldier = makeSoldier(rank: AuthorizedRank.e6); final soldier = makeSoldier(rank: AuthorizedRank.e6);
final outcome = service.assign(slot, soldier); final outcome = service.assign(slot, soldier);
expect(outcome.isSuccess, isTrue); expect(outcome.isSuccess, isTrue);
@ -151,15 +151,15 @@ void main() {
test('all FMC returns green', () { test('all FMC returns green', () {
final soldiers = [ final soldiers = [
makeSoldier(id: 'p1', readiness: ReadinessState.fullyMissionCapable), makeSoldier(),
makeSoldier(id: 'p2', readiness: ReadinessState.fullyMissionCapable), makeSoldier(id: 'p2'),
]; ];
expect(service.aggregateReadiness(soldiers), ReadinessColor.green); expect(service.aggregateReadiness(soldiers), ReadinessColor.green);
}); });
test('any NMC returns red', () { test('any NMC returns red', () {
final soldiers = [ final soldiers = [
makeSoldier(id: 'p1', readiness: ReadinessState.fullyMissionCapable), makeSoldier(),
makeSoldier(id: 'p2', readiness: ReadinessState.notMissionCapable), makeSoldier(id: 'p2', readiness: ReadinessState.notMissionCapable),
]; ];
expect(service.aggregateReadiness(soldiers), ReadinessColor.red); expect(service.aggregateReadiness(soldiers), ReadinessColor.red);
@ -167,14 +167,14 @@ void main() {
test('mixed FMC and PMC returns amber', () { test('mixed FMC and PMC returns amber', () {
final soldiers = [ final soldiers = [
makeSoldier(id: 'p1', readiness: ReadinessState.fullyMissionCapable), makeSoldier(),
makeSoldier(id: 'p2', readiness: ReadinessState.partiallyMissionCapable), makeSoldier(id: 'p2', readiness: ReadinessState.partiallyMissionCapable),
]; ];
expect(service.aggregateReadiness(soldiers), ReadinessColor.amber); expect(service.aggregateReadiness(soldiers), ReadinessColor.amber);
}); });
test('single PMC soldier returns amber', () { test('single PMC soldier returns amber', () {
final soldiers = [makeSoldier(id: 'p1', readiness: ReadinessState.partiallyMissionCapable)]; final soldiers = [makeSoldier(readiness: ReadinessState.partiallyMissionCapable)];
expect(service.aggregateReadiness(soldiers), ReadinessColor.amber); expect(service.aggregateReadiness(soldiers), ReadinessColor.amber);
}); });
}); });
@ -185,14 +185,14 @@ void main() {
group('UnitTreeService integration via AssignmentService', () { group('UnitTreeService integration via AssignmentService', () {
test('validateReparent is accessible through treeService', () { test('validateReparent is accessible through treeService', () {
final bde = UnitNode( const bde = UnitNode(
unitId: 'bde', unitId: 'bde',
echelon: Echelon.brigade, echelon: Echelon.brigade,
branch: DoctrinalBranch.infantry, branch: DoctrinalBranch.infantry,
name: 'Brigade', name: 'Brigade',
isKeyLeader: false, isKeyLeader: false,
); );
final bn = UnitNode( const bn = UnitNode(
unitId: 'bn', unitId: 'bn',
echelon: Echelon.battalion, echelon: Echelon.battalion,
branch: DoctrinalBranch.infantry, branch: DoctrinalBranch.infantry,

View File

@ -18,8 +18,7 @@ void main() {
newParentId: 'unit-2', newParentId: 'unit-2',
previousParentId: 'unit-0', previousParentId: 'unit-0',
), ),
occurredAt: DateTime.utc(2026, 8, 9, 10, 0, 0), occurredAt: DateTime.utc(2026, 8, 9, 10),
isSynced: false,
); );
final json = event.toJson(); final json = event.toJson();
@ -144,16 +143,19 @@ void main() {
test('allEvents is unmodifiable', () { test('allEvents is unmodifiable', () {
store.append(_makeEvent('e1', CommandEventType.attachNode)); store.append(_makeEvent('e1', CommandEventType.attachNode));
expect(() => store.allEvents.add(_makeEvent('e2', CommandEventType.detachNode)), expect(
throwsUnsupportedError); () => store.allEvents.add(_makeEvent('e2', CommandEventType.detachNode)),
throwsUnsupportedError,
);
}); });
test('pendingEvents excludes synced events', () { test('pendingEvents excludes synced events', () {
final e1 = _makeEvent('e1', CommandEventType.attachNode); final e1 = _makeEvent('e1', CommandEventType.attachNode);
final e2 = _makeEvent('e2', CommandEventType.detachNode); final e2 = _makeEvent('e2', CommandEventType.detachNode);
store.append(e1); store
store.append(e2); ..append(e1)
store.markSynced('e1'); ..append(e2)
..markSynced('e1');
expect(store.pendingCount, 1); expect(store.pendingCount, 1);
expect(store.pendingEvents.first.eventId, 'e2'); expect(store.pendingEvents.first.eventId, 'e2');
@ -170,17 +172,19 @@ void main() {
}); });
test('markAllSynced marks every event', () { test('markAllSynced marks every event', () {
store.append(_makeEvent('e1', CommandEventType.attachNode)); store
store.append(_makeEvent('e2', CommandEventType.assignPersonnel)); ..append(_makeEvent('e1', CommandEventType.attachNode))
store.append(_makeEvent('e3', CommandEventType.modifySlot)); ..append(_makeEvent('e2', CommandEventType.assignPersonnel))
store.markAllSynced(); ..append(_makeEvent('e3', CommandEventType.modifySlot))
..markAllSynced();
expect(store.pendingCount, 0); expect(store.pendingCount, 0);
}); });
test('exportPendingToJson produces valid JSON array', () { test('exportPendingToJson produces valid JSON array', () {
store.append(_makeEvent('e1', CommandEventType.attachNode)); store
store.append(_makeEvent('e2', CommandEventType.detachNode)); ..append(_makeEvent('e1', CommandEventType.attachNode))
store.markSynced('e1'); // e1 is synced, only e2 should appear ..append(_makeEvent('e2', CommandEventType.detachNode))
..markSynced('e1'); // e1 is synced, only e2 should appear
final json = store.exportPendingToJson(); final json = store.exportPendingToJson();
final decoded = jsonDecode(json) as List<dynamic>; final decoded = jsonDecode(json) as List<dynamic>;
@ -189,9 +193,10 @@ void main() {
}); });
test('clear removes all events', () { test('clear removes all events', () {
store.append(_makeEvent('e1', CommandEventType.attachNode)); store
store.append(_makeEvent('e2', CommandEventType.detachNode)); ..append(_makeEvent('e1', CommandEventType.attachNode))
store.clear(); ..append(_makeEvent('e2', CommandEventType.detachNode))
..clear();
expect(store.eventCount, 0); expect(store.eventCount, 0);
}); });
@ -230,8 +235,9 @@ void main() {
}); });
test('syncIfOnline transmits pending events when online', () async { test('syncIfOnline transmits pending events when online', () async {
store.append(_makeEvent('e1', CommandEventType.attachNode)); store
store.append(_makeEvent('e2', CommandEventType.assignPersonnel)); ..append(_makeEvent('e1', CommandEventType.attachNode))
..append(_makeEvent('e2', CommandEventType.assignPersonnel));
sync.isOnline = true; sync.isOnline = true;
final count = await sync.syncIfOnline(); final count = await sync.syncIfOnline();
@ -249,9 +255,10 @@ void main() {
test('already-synced events are not re-transmitted', () async { test('already-synced events are not re-transmitted', () async {
final e1 = _makeEvent('e1', CommandEventType.attachNode); final e1 = _makeEvent('e1', CommandEventType.attachNode);
final e2 = _makeEvent('e2', CommandEventType.detachNode); final e2 = _makeEvent('e2', CommandEventType.detachNode);
store.append(e1); store
store.append(e2); ..append(e1)
store.markSynced('e1'); ..append(e2)
..markSynced('e1');
final count = await sync.syncIfOnline(); final count = await sync.syncIfOnline();
expect(count, 1); // only e2 was pending expect(count, 1); // only e2 was pending

View File

@ -89,7 +89,7 @@ void main() {
mos: '68W', mos: '68W',
assignedPersonnelId: 'p1', assignedPersonnelId: 'p1',
); );
final soldier = makeSoldier(id: 'p1', mos: '11B', assignedSlotId: 's1'); final soldier = makeSoldier(id: 'p1', assignedSlotId: 's1');
final findings = engine.validate( final findings = engine.validate(
units: [unit], units: [unit],
@ -109,10 +109,9 @@ void main() {
final slot = makeSlot( final slot = makeSlot(
slotId: 's1', slotId: 's1',
unitId: 'u1', unitId: 'u1',
mos: '11B',
assignedPersonnelId: 'p1', assignedPersonnelId: 'p1',
); );
final soldier = makeSoldier(id: 'p1', mos: '11B', assignedSlotId: 's1'); final soldier = makeSoldier(id: 'p1', assignedSlotId: 's1');
final findings = engine.validate( final findings = engine.validate(
units: [unit], units: [unit],
@ -174,7 +173,6 @@ void main() {
final slot = makeSlot( final slot = makeSlot(
slotId: 's1', slotId: 's1',
unitId: 'u1', unitId: 'u1',
rank: AuthorizedRank.e5,
assignedPersonnelId: 'p1', assignedPersonnelId: 'p1',
); );
final soldier = makeSoldier( final soldier = makeSoldier(
@ -240,7 +238,7 @@ void main() {
test('no V003 for non-key-leader vacant slots', () { test('no V003 for non-key-leader vacant slots', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot = makeSlot(slotId: 's1', unitId: 'u1', isKeyLeader: false); final slot = makeSlot(slotId: 's1', unitId: 'u1');
final findings = engine.validate( final findings = engine.validate(
units: [unit], units: [unit],
@ -259,7 +257,7 @@ void main() {
group('V004 — missing combat medic', () { group('V004 — missing combat medic', () {
test('flags platoon with no 68W slot as advisory', () { test('flags platoon with no 68W slot as advisory', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.platoon); final unit = makeUnit(id: 'u1', echelon: Echelon.platoon);
final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '11B'); final slot = makeSlot(slotId: 's1', unitId: 'u1');
final findings = engine.validate( final findings = engine.validate(
units: [unit], units: [unit],
@ -289,7 +287,7 @@ void main() {
test('no V004 for non-platoon echelons', () { test('no V004 for non-platoon echelons', () {
final unit = makeUnit(id: 'u1', echelon: Echelon.squad); final unit = makeUnit(id: 'u1', echelon: Echelon.squad);
final slot = makeSlot(slotId: 's1', unitId: 'u1', mos: '11B'); final slot = makeSlot(slotId: 's1', unitId: 'u1');
final findings = engine.validate( final findings = engine.validate(
units: [unit], units: [unit],

View File

@ -4,13 +4,13 @@ import 'package:uto_manager/domain/services/template_instantiator.dart';
void main() { void main() {
// Use a deterministic counter-based ID generator for tests. // Use a deterministic counter-based ID generator for tests.
int _counter = 0; var counter0 = 0;
String nextId() => 'id-${_counter++}'; String nextId() => 'id-${counter0++}';
late TemplateInstantiator instantiator; late TemplateInstantiator instantiator;
setUp(() { setUp(() {
_counter = 0; counter0 = 0;
instantiator = TemplateInstantiator(idGenerator: nextId); instantiator = TemplateInstantiator(idGenerator: nextId);
}); });
@ -109,12 +109,12 @@ void main() {
group('parent-child relationships', () { group('parent-child relationships', () {
test('child unit has correct parentUnitId', () { test('child unit has correct parentUnitId', () {
final def = UnitDefinition( const def = UnitDefinition(
name: 'Battalion', name: 'Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
children: [ children: [
const UnitDefinition( UnitDefinition(
name: 'Alpha Company', name: 'Alpha Company',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
@ -131,7 +131,7 @@ void main() {
}); });
test('deeply nested tree has correct parent chain', () { test('deeply nested tree has correct parent chain', () {
final def = UnitDefinition( const def = UnitDefinition(
name: 'Brigade', name: 'Brigade',
echelon: 'brigade', echelon: 'brigade',
branch: 'infantry', branch: 'infantry',
@ -141,7 +141,7 @@ void main() {
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
children: [ children: [
const UnitDefinition( UnitDefinition(
name: 'Company', name: 'Company',
echelon: 'company', echelon: 'company',
branch: 'infantry', branch: 'infantry',
@ -164,14 +164,14 @@ void main() {
test('all generated IDs are unique', () { test('all generated IDs are unique', () {
// Use UUID-style generator to verify uniqueness. // Use UUID-style generator to verify uniqueness.
int counter = 0; var counter = 0;
final inst = TemplateInstantiator(idGenerator: () => 'uid-${counter++}'); final inst = TemplateInstantiator(idGenerator: () => 'uid-${counter++}');
final def = UnitDefinition( const def = UnitDefinition(
name: 'Brigade', name: 'Brigade',
echelon: 'brigade', echelon: 'brigade',
branch: 'infantry', branch: 'infantry',
slots: const [ slots: [
SlotDefinition( SlotDefinition(
positionTitle: 'Commander', positionTitle: 'Commander',
dutyMos: '11A', dutyMos: '11A',
@ -179,7 +179,7 @@ void main() {
), ),
], ],
children: [ children: [
const UnitDefinition( UnitDefinition(
name: 'Battalion', name: 'Battalion',
echelon: 'battalion', echelon: 'battalion',
branch: 'infantry', branch: 'infantry',
@ -293,7 +293,7 @@ void main() {
'abct', 'abct',
'cab_heavy', 'cab_heavy',
'cab_light', 'cab_light',
])); ]),);
}); });
test('DoctrinalTemplates.displayName returns non-empty strings', () { test('DoctrinalTemplates.displayName returns non-empty strings', () {

View File

@ -14,7 +14,7 @@ void main() {
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
UnitNode _makeUnit(String id, Echelon echelon, {String? parentId}) => UnitNode( UnitNode makeUnit(String id, Echelon echelon, {String? parentId}) => UnitNode(
unitId: id, unitId: id,
echelon: echelon, echelon: echelon,
branch: DoctrinalBranch.infantry, branch: DoctrinalBranch.infantry,
@ -23,7 +23,7 @@ void main() {
isKeyLeader: false, isKeyLeader: false,
); );
PositionSlot _makeSlot( PositionSlot makeSlot(
String id, String id,
String unitId, { String unitId, {
bool isKeyLeader = false, bool isKeyLeader = false,
@ -89,8 +89,8 @@ void main() {
test('validate() detects vacant key leader (V003)', () async { test('validate() detects vacant key leader (V003)', () async {
await runner.start(); await runner.start();
final unit = _makeUnit('u1', Echelon.squad); final unit = makeUnit('u1', Echelon.squad);
final slot = _makeSlot('sl1', 'u1', isKeyLeader: true); // vacant final slot = makeSlot('sl1', 'u1', isKeyLeader: true); // vacant
final response = await runner.validate( final response = await runner.validate(
units: [unit], units: [unit],
@ -104,8 +104,8 @@ void main() {
test('validate() detects missing medic in platoon (V004)', () async { test('validate() detects missing medic in platoon (V004)', () async {
await runner.start(); await runner.start();
final unit = _makeUnit('u1', Echelon.platoon); final unit = makeUnit('u1', Echelon.platoon);
final slot = _makeSlot('sl1', 'u1'); // no 68W slot final slot = makeSlot('sl1', 'u1'); // no 68W slot
final response = await runner.validate( final response = await runner.validate(
units: [unit], units: [unit],
@ -120,7 +120,7 @@ void main() {
test('validate() returns clean result for fully-staffed valid unit', () async { test('validate() returns clean result for fully-staffed valid unit', () async {
await runner.start(); await runner.start();
// A squad with a filled key leader slot and no hierarchy violations. // A squad with a filled key leader slot and no hierarchy violations.
final unit = _makeUnit('u1', Echelon.squad); final unit = makeUnit('u1', Echelon.squad);
final keyLeaderSlot = PositionSlot( final keyLeaderSlot = PositionSlot(
slotId: 'sl1', slotId: 'sl1',
unitId: 'u1', unitId: 'u1',
@ -135,7 +135,6 @@ void main() {
name: 'SSG Smith', name: 'SSG Smith',
primaryMos: DutyMos.unvalidated('11B'), primaryMos: DutyMos.unvalidated('11B'),
payGrade: AuthorizedRank.e6, payGrade: AuthorizedRank.e6,
qualifications: const [],
readinessState: ReadinessState.fullyMissionCapable, readinessState: ReadinessState.fullyMissionCapable,
); );
@ -153,8 +152,8 @@ void main() {
test('validate() detects structural hierarchy violation (V006)', () async { test('validate() detects structural hierarchy violation (V006)', () async {
await runner.start(); await runner.start();
// Squad directly under Brigade — invalid. // Squad directly under Brigade — invalid.
final brigade = _makeUnit('bde', Echelon.brigade); final brigade = makeUnit('bde', Echelon.brigade);
final squad = _makeUnit('sq1', Echelon.squad, parentId: 'bde'); final squad = makeUnit('sq1', Echelon.squad, parentId: 'bde');
final response = await runner.validate( final response = await runner.validate(
units: [brigade, squad], units: [brigade, squad],
@ -173,13 +172,13 @@ void main() {
test('validate() results match synchronous engine output', () async { test('validate() results match synchronous engine output', () async {
await runner.start(); await runner.start();
final units = [ final units = [
_makeUnit('bde', Echelon.brigade), makeUnit('bde', Echelon.brigade),
_makeUnit('bn', Echelon.battalion, parentId: 'bde'), makeUnit('bn', Echelon.battalion, parentId: 'bde'),
_makeUnit('co', Echelon.company, parentId: 'bn'), makeUnit('co', Echelon.company, parentId: 'bn'),
_makeUnit('plt', Echelon.platoon, parentId: 'co'), makeUnit('plt', Echelon.platoon, parentId: 'co'),
]; ];
final slots = [ final slots = [
_makeSlot('sl1', 'plt', isKeyLeader: true), // vacant key leader makeSlot('sl1', 'plt', isKeyLeader: true), // vacant key leader
]; ];
final isolateResponse = await runner.validate( final isolateResponse = await runner.validate(

View File

@ -1,10 +1,10 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:uto_manager/domain/entities/authorized_rank.dart';
import 'package:uto_manager/domain/entities/doctrinal_branch.dart'; import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
import 'package:uto_manager/domain/entities/echelon.dart'; import 'package:uto_manager/domain/entities/echelon.dart';
import 'package:uto_manager/domain/entities/position_slot.dart'; import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/domain/entities/unit_node.dart'; import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/value_objects/duty_mos.dart'; import 'package:uto_manager/domain/value_objects/duty_mos.dart';
import 'package:uto_manager/domain/entities/authorized_rank.dart';
import 'package:uto_manager/infrastructure/codecs/pdf_chart_exporter.dart'; import 'package:uto_manager/infrastructure/codecs/pdf_chart_exporter.dart';
void main() { void main() {
@ -14,7 +14,7 @@ void main() {
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
UnitNode _makeUnit( UnitNode makeUnit(
String id, String id,
Echelon echelon, { Echelon echelon, {
String? parentId, String? parentId,
@ -29,7 +29,7 @@ void main() {
isKeyLeader: false, isKeyLeader: false,
); );
PositionSlot _makeSlot(String id, String unitId) => PositionSlot( PositionSlot makeSlot(String id, String unitId) => PositionSlot(
slotId: id, slotId: id,
unitId: unitId, unitId: unitId,
positionTitle: 'Rifleman', positionTitle: 'Rifleman',
@ -111,7 +111,7 @@ void main() {
group('PdfChartExporter — tree rendering', () { group('PdfChartExporter — tree rendering', () {
test('single root unit renders without error', () { test('single root unit renders without error', () {
final units = [_makeUnit('u1', Echelon.platoon, name: '1st Platoon')]; final units = [makeUnit('u1', Echelon.platoon, name: '1st Platoon')];
final bytes = exporter.export( final bytes = exporter.export(
units: units, units: units,
slots: const [], slots: const [],
@ -123,9 +123,9 @@ void main() {
test('parent-child tree renders connector lines', () { test('parent-child tree renders connector lines', () {
final units = [ final units = [
_makeUnit('u1', Echelon.platoon, name: '1st Platoon'), makeUnit('u1', Echelon.platoon, name: '1st Platoon'),
_makeUnit('u2', Echelon.squad, parentId: 'u1', name: '1st Squad'), makeUnit('u2', Echelon.squad, parentId: 'u1', name: '1st Squad'),
_makeUnit('u3', Echelon.squad, parentId: 'u1', name: '2nd Squad'), makeUnit('u3', Echelon.squad, parentId: 'u1', name: '2nd Squad'),
]; ];
final bytes = exporter.export( final bytes = exporter.export(
units: units, units: units,
@ -139,11 +139,11 @@ void main() {
}); });
test('all echelon levels render without error', () { test('all echelon levels render without error', () {
final echelons = Echelon.values; const echelons = Echelon.values;
final units = echelons final units = echelons
.asMap() .asMap()
.entries .entries
.map((e) => _makeUnit('u${e.key}', e.value, name: e.value.displayName)) .map((e) => makeUnit('u${e.key}', e.value, name: e.value.displayName))
.toList(); .toList();
expect( expect(
@ -158,8 +158,8 @@ void main() {
test('unit names appear in PDF content stream', () { test('unit names appear in PDF content stream', () {
final units = [ final units = [
_makeUnit('u1', Echelon.company, name: 'Alpha Company'), makeUnit('u1', Echelon.company, name: 'Alpha Company'),
_makeUnit('u2', Echelon.platoon, parentId: 'u1', name: '1st Platoon'), makeUnit('u2', Echelon.platoon, parentId: 'u1', name: '1st Platoon'),
]; ];
final bytes = exporter.export( final bytes = exporter.export(
units: units, units: units,
@ -173,8 +173,8 @@ void main() {
}); });
test('slots are accepted without error', () { test('slots are accepted without error', () {
final units = [_makeUnit('u1', Echelon.squad)]; final units = [makeUnit('u1', Echelon.squad)];
final slots = List.generate(9, (i) => _makeSlot('sl$i', 'u1')); final slots = List.generate(9, (i) => makeSlot('sl$i', 'u1'));
expect( expect(
() => exporter.export( () => exporter.export(
units: units, units: units,
@ -222,7 +222,6 @@ void main() {
units: const [], units: const [],
slots: const [], slots: const [],
unitDesignation: 'Test', unitDesignation: 'Test',
pageSize: PdfPageSize.tabloid,
); );
final content = String.fromCharCodes(bytes); final content = String.fromCharCodes(bytes);
expect(content, contains('792.0')); expect(content, contains('792.0'));
@ -265,24 +264,27 @@ void main() {
group('PdfChartExporter — large tree', () { group('PdfChartExporter — large tree', () {
test('50-node tree exports without error', () { test('50-node tree exports without error', () {
final units = <UnitNode>[]; final units = <UnitNode>[makeUnit('root', Echelon.company, name: 'Alpha Co')];
units.add(_makeUnit('root', Echelon.company, name: 'Alpha Co'));
for (var i = 0; i < 3; i++) { for (var i = 0; i < 3; i++) {
units.add(_makeUnit('plt$i', Echelon.platoon, parentId: 'root', name: '${i + 1}st Plt')); units.add(makeUnit('plt$i', Echelon.platoon, parentId: 'root', name: '${i + 1}st Plt'));
for (var j = 0; j < 3; j++) { for (var j = 0; j < 3; j++) {
units.add(_makeUnit( units.add(
makeUnit(
'sq${i}_$j', 'sq${i}_$j',
Echelon.squad, Echelon.squad,
parentId: 'plt$i', parentId: 'plt$i',
name: '${j + 1}st Sq', name: '${j + 1}st Sq',
)); ),
);
for (var k = 0; k < 2; k++) { for (var k = 0; k < 2; k++) {
units.add(_makeUnit( units.add(
makeUnit(
'tm${i}_${j}_$k', 'tm${i}_${j}_$k',
Echelon.team, Echelon.team,
parentId: 'sq${i}_$j', parentId: 'sq${i}_$j',
name: '${k + 1} Team', name: '${k + 1} Team',
)); ),
);
} }
} }
} }

View File

@ -18,7 +18,7 @@ void main() {
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
UnitNode _makeUnit(String id, {String? parentId}) => UnitNode( UnitNode makeUnit(String id, {String? parentId}) => UnitNode(
unitId: id, unitId: id,
echelon: Echelon.platoon, echelon: Echelon.platoon,
branch: DoctrinalBranch.infantry, branch: DoctrinalBranch.infantry,
@ -27,7 +27,7 @@ void main() {
isKeyLeader: false, isKeyLeader: false,
); );
PositionSlot _makeSlot(String id, String unitId) => PositionSlot( PositionSlot makeSlot(String id, String unitId) => PositionSlot(
slotId: id, slotId: id,
unitId: unitId, unitId: unitId,
positionTitle: 'Rifleman', positionTitle: 'Rifleman',
@ -36,12 +36,11 @@ void main() {
isKeyLeader: false, isKeyLeader: false,
); );
PersonnelRecord _makeSoldier(String id) => PersonnelRecord( PersonnelRecord makeSoldier(String id) => PersonnelRecord(
soldierId: id, soldierId: id,
name: 'SGT Test', name: 'SGT Test',
primaryMos: DutyMos.unvalidated('11B'), primaryMos: DutyMos.unvalidated('11B'),
payGrade: AuthorizedRank.e5, payGrade: AuthorizedRank.e5,
qualifications: const [],
readinessState: ReadinessState.fullyMissionCapable, readinessState: ReadinessState.fullyMissionCapable,
); );
@ -51,14 +50,14 @@ void main() {
group('ProtobufCodec — encode/decode', () { group('ProtobufCodec — encode/decode', () {
test('encodes to non-empty Uint8List', () { test('encodes to non-empty Uint8List', () {
final units = [_makeUnit('u1')]; final units = [makeUnit('u1')];
final bytes = codec.encode(units: units, slots: const [], personnel: const []); final bytes = codec.encode(units: units, slots: const [], personnel: const []);
expect(bytes, isA<Uint8List>()); expect(bytes, isA<Uint8List>());
expect(bytes.length, greaterThan(12)); expect(bytes.length, greaterThan(12));
}); });
test('header contains correct magic bytes "UTOB"', () { test('header contains correct magic bytes "UTOB"', () {
final bytes = codec.encode(units: [_makeUnit('u1')], slots: const [], personnel: const []); final bytes = codec.encode(units: [makeUnit('u1')], slots: const [], personnel: const []);
// Magic: 0x55544F42 // Magic: 0x55544F42
expect(bytes[0], 0x55); expect(bytes[0], 0x55);
expect(bytes[1], 0x54); expect(bytes[1], 0x54);
@ -67,7 +66,7 @@ void main() {
}); });
test('header contains version 1', () { test('header contains version 1', () {
final bytes = codec.encode(units: [_makeUnit('u1')], slots: const [], personnel: const []); final bytes = codec.encode(units: [makeUnit('u1')], slots: const [], personnel: const []);
// Version at bytes 4–7 // Version at bytes 4–7
expect(bytes[4], 0x00); expect(bytes[4], 0x00);
expect(bytes[5], 0x00); expect(bytes[5], 0x00);
@ -76,7 +75,7 @@ void main() {
}); });
test('round-trip preserves units', () { test('round-trip preserves units', () {
final units = [_makeUnit('u1'), _makeUnit('u2', parentId: 'u1')]; final units = [makeUnit('u1'), makeUnit('u2', parentId: 'u1')];
final bytes = codec.encode(units: units, slots: const [], personnel: const []); final bytes = codec.encode(units: units, slots: const [], personnel: const []);
final snapshot = codec.decode(bytes); final snapshot = codec.decode(bytes);
@ -86,8 +85,8 @@ void main() {
}); });
test('round-trip preserves slots', () { test('round-trip preserves slots', () {
final units = [_makeUnit('u1')]; final units = [makeUnit('u1')];
final slots = [_makeSlot('sl1', 'u1'), _makeSlot('sl2', 'u1')]; final slots = [makeSlot('sl1', 'u1'), makeSlot('sl2', 'u1')];
final bytes = codec.encode(units: units, slots: slots, personnel: const []); final bytes = codec.encode(units: units, slots: slots, personnel: const []);
final snapshot = codec.decode(bytes); final snapshot = codec.decode(bytes);
@ -96,8 +95,8 @@ void main() {
}); });
test('round-trip preserves personnel', () { test('round-trip preserves personnel', () {
final units = [_makeUnit('u1')]; final units = [makeUnit('u1')];
final personnel = [_makeSoldier('s1'), _makeSoldier('s2')]; final personnel = [makeSoldier('s1'), makeSoldier('s2')];
final bytes = codec.encode(units: units, slots: const [], personnel: personnel); final bytes = codec.encode(units: units, slots: const [], personnel: personnel);
final snapshot = codec.decode(bytes); final snapshot = codec.decode(bytes);
@ -116,9 +115,9 @@ void main() {
test('binary output is smaller than JSON for non-trivial tree', () { test('binary output is smaller than JSON for non-trivial tree', () {
final units = List.generate( final units = List.generate(
20, 20,
(i) => _makeUnit('u$i', parentId: i > 0 ? 'u${i - 1}' : null), (i) => makeUnit('u$i', parentId: i > 0 ? 'u${i - 1}' : null),
); );
final slots = List.generate(40, (i) => _makeSlot('sl$i', 'u${i % 20}')); final slots = List.generate(40, (i) => makeSlot('sl$i', 'u${i % 20}'));
final binaryBytes = codec.encode(units: units, slots: slots, personnel: const []); final binaryBytes = codec.encode(units: units, slots: slots, personnel: const []);
final jsonLen = final jsonLen =
@ -151,14 +150,14 @@ void main() {
test('decode throws FormatException for wrong version', () { test('decode throws FormatException for wrong version', () {
// Build a valid header but with version = 99. // Build a valid header but with version = 99.
final bytes = codec.encode(units: [_makeUnit('u1')], slots: const [], personnel: const []); final bytes = codec.encode(units: [makeUnit('u1')], slots: const [], personnel: const []);
final tampered = Uint8List.fromList(bytes); final tampered = Uint8List.fromList(bytes);
tampered[7] = 99; // version byte (big-endian, LSB) tampered[7] = 99; // version byte (big-endian, LSB)
expect(() => codec.decode(tampered), throwsA(isA<FormatException>())); expect(() => codec.decode(tampered), throwsA(isA<FormatException>()));
}); });
test('decode throws FormatException for truncated payload', () { test('decode throws FormatException for truncated payload', () {
final bytes = codec.encode(units: [_makeUnit('u1')], slots: const [], personnel: const []); final bytes = codec.encode(units: [makeUnit('u1')], slots: const [], personnel: const []);
// Truncate to just the header. // Truncate to just the header.
final truncated = bytes.sublist(0, 12); final truncated = bytes.sublist(0, 12);
expect(() => codec.decode(truncated), throwsA(isA<FormatException>())); expect(() => codec.decode(truncated), throwsA(isA<FormatException>()));
@ -179,7 +178,7 @@ void main() {
}); });
test('ratio < 1.0 for compressed data', () { test('ratio < 1.0 for compressed data', () {
final units = List.generate(10, (i) => _makeUnit('u$i')); final units = List.generate(10, (i) => makeUnit('u$i'));
final bytes = codec.encode(units: units, slots: const [], personnel: const []); final bytes = codec.encode(units: units, slots: const [], personnel: const []);
final jsonLen = final jsonLen =
const UnitTreeCodec().encode(units: units, slots: const [], personnel: const []).length; const UnitTreeCodec().encode(units: units, slots: const [], personnel: const []).length;

View File

@ -15,7 +15,7 @@ void main() {
// Fixtures // Fixtures
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
final brigade = UnitNode( const brigade = UnitNode(
unitId: 'bde-1', unitId: 'bde-1',
echelon: Echelon.brigade, echelon: Echelon.brigade,
branch: DoctrinalBranch.infantry, branch: DoctrinalBranch.infantry,
@ -23,7 +23,7 @@ void main() {
isKeyLeader: false, isKeyLeader: false,
); );
final battalion = UnitNode( const battalion = UnitNode(
unitId: 'bn-1', unitId: 'bn-1',
echelon: Echelon.battalion, echelon: Echelon.battalion,
branch: DoctrinalBranch.infantry, branch: DoctrinalBranch.infantry,
@ -170,8 +170,7 @@ void main() {
}); });
test('throws FormatException for missing version', () { test('throws FormatException for missing version', () {
final map = codec.encodeToMap(units: [], slots: [], personnel: []); final map = codec.encodeToMap(units: [], slots: [], personnel: [])..remove('version');
map.remove('version');
expect( expect(
() => codec.decodeFromMap(map), () => codec.decodeFromMap(map),
@ -224,7 +223,7 @@ void main() {
primaryMos: DutyMos.unvalidated('11B'), primaryMos: DutyMos.unvalidated('11B'),
payGrade: AuthorizedRank.e5, payGrade: AuthorizedRank.e5,
readinessState: ReadinessState.fullyMissionCapable, readinessState: ReadinessState.fullyMissionCapable,
qualifications: ['Airborne', 'Ranger', 'SERE'], qualifications: const ['Airborne', 'Ranger', 'SERE'],
); );
final json = codec.encode( final json = codec.encode(
units: [], units: [],

View File

@ -28,7 +28,7 @@ void main() {
final unitRepo = DriftUnitRepository(dao: db.unitDao, db: db); final unitRepo = DriftUnitRepository(dao: db.unitDao, db: db);
final personnelRepo = DriftPersonnelRepository(dao: db.personnelDao); final personnelRepo = DriftPersonnelRepository(dao: db.personnelDao);
final slotRepo = DriftPositionSlotRepository(dao: db.positionSlotDao); final slotRepo = DriftPositionSlotRepository(dao: db.positionSlotDao);
final assignmentService = AssignmentService(treeService: treeService); const assignmentService = AssignmentService(treeService: treeService);
// Create cubits but do NOT call startWatching() — avoids live streams. // Create cubits but do NOT call startWatching() — avoids live streams.
unitTreeCubit = UnitTreeCubit( unitTreeCubit = UnitTreeCubit(