feat(scaffold): create product workspace + Rust crates + Flutter app

Implements the canonical implementation directory layout adopted by
DEC-022 (register v0.9.5). Closes the scaffolding phase; no PoC code
has been promoted in yet (per proof-of-concept-plan.md §4 a PoC is
not product code unless explicitly promoted).

Rust workspace
==============

Top-level Cargo.toml declares seven workspace members:

  core/chanora_core           top-level Rust API + orchestration
  crates/chanora_protocol     tsclientlib isolation (SAD-067, SysDes-011/029)
  crates/chanora_state        state sync, reducers, deltas
  crates/chanora_audio        capture, DSP, Opus, jitter, mixer, playback
  crates/chanora_storage      non-secret DB + platform secure store
  crates/chanora_diagnostics  logs, redaction, export
  crates/chanora_bridge       typed Flutter/Rust DTOs

Workspace-wide pins:
  license      = "MIT OR Apache-2.0"   (DEC-020)
  rust-version = "1.95"
  edition      = "2021"

The Flutter app (apps/chanora_flutter) is NOT a Cargo workspace
member; it is owned by the Flutter / Gradle toolchain and is in the
workspace exclude array along with every poc/* spike.

Each crate ships:
  * a Cargo.toml referring to workspace.dependencies pins;
  * a lib.rs with #![forbid(unsafe_code)] + #![warn(missing_docs)],
    a typed Error enum, and the public types relevant to the
    subsystem's role per SAD §7.2;
  * minimal unit tests so
running 1 test
test tests::defaults_match_decisions ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::it_compiles ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::session_can_be_constructed ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::marker_matches_poc ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::it_compiles ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::state_transitions_compile ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 1 test
test tests::it_compiles ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s is non-empty.

chanora_core's CoreError type-wraps every subsystem error via
#[from] so callers can match on origin without parsing strings.

chanora_audio's AudioEffects struct defaults all four effects to
true, matching DEC-007 (AEC), DEC-008 (AGC), DEC-009 (NS),
DEC-010 (HPF). A unit test pins this so a future regression that
flips a default fails immediately.

chanora_diagnostics exports REDACTION_MARKER = "[REDACTED]",
identical to the PoC's marker so audit grep patterns survive the
promotion.

chanora_bridge's BridgeError is Serialize + Deserialize so it can
flow across the FRB 2.x boundary (DEC-014).

Empirical verification: cargo check --workspace clean, cargo test
--workspace clean (7 unit tests + 7 doc-test runners, all passing)
against Rust 1.95.0 stable.

Flutter app
===========

apps/chanora_flutter created with .

DEC-004 applied: minSdk overridden to 28 in
android/app/build.gradle.kts with a comment that points back at the
decision register and forbids lowering it without re-opening DEC-004.

DEC-015 applied: shipped English + Simplified Chinese at MVP.
  - pubspec.yaml gains flutter_localizations + intl + generate:true.
  - l10n.yaml emits lib/l10n/generated/AppL10n (no synthetic package
    — that was removed in Flutter 3.41+).
  - lib/l10n/app_en.arb is the source of truth; lib/l10n/app_zh.arb
    mirrors the key set in zh-Hans. ARB metadata explicitly
    reaffirms ADR-008: server-provided content (channel names,
    nicknames, welcome banners) is preserved verbatim and never
    translated.

lib/main.dart and test/widget_test.dart were rewritten from the
template counter into a minimal localized scaffold that proves
both locales render correctly.

Empirical verification:  reports no issues;
 runs the two locale smoke tests and both pass.
This commit is contained in:
EdisonJwa
2026-05-14 21:01:59 +08:00
parent 0f418f1d7e
commit 974dda9601
57 changed files with 2292 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
{
"@@locale": "en",
"@@x-source-of-truth": "DEC-015 (register v0.9.5). Template ARB for English. Other locales must reference these keys. Server-provided content is NOT translated (ADR-008 / DEC-015).",
"appTitle": "Chanora",
"@appTitle": {
"description": "Application title shown in launchers and the app bar. The product name `Chanora` is fixed by DEC-018 and must not be translated."
},
"homeGreeting": "Welcome to Chanora",
"@homeGreeting": {
"description": "Greeting shown on the empty home screen before any server is connected."
},
"homeNotProductionReadyBanner": "Chanora is currently in early development. This build is not production-ready.",
"@homeNotProductionReadyBanner": {
"description": "Plain-language banner informing testers that this build is not for end-user use. Mirrors README.md's status line."
},
"connectAction": "Connect to a server",
"@connectAction": {
"description": "Label for the primary action that opens the connect-to-server flow."
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"@@locale": "zh",
"@@x-source-of-truth": "DEC-015 (register v0.9.5). Simplified Chinese translations for the MVP key set. Keys must match app_en.arb; server-provided content is NOT translated (ADR-008 / DEC-015).",
"appTitle": "Chanora",
"homeGreeting": "欢迎使用 Chanora",
"homeNotProductionReadyBanner": "Chanora 目前处于早期开发阶段,此版本尚未达到正式发布质量。",
"connectAction": "连接到服务器"
}
@@ -0,0 +1,156 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppL10n
/// returned by `AppL10n.of(context)`.
///
/// Applications need to include `AppL10n.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'generated/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppL10n.localizationsDelegates,
/// supportedLocales: AppL10n.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppL10n.supportedLocales
/// property.
abstract class AppL10n {
AppL10n(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppL10n of(BuildContext context) {
return Localizations.of<AppL10n>(context, AppL10n)!;
}
static const LocalizationsDelegate<AppL10n> delegate = _AppL10nDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('zh'),
];
/// Application title shown in launchers and the app bar. The product name `Chanora` is fixed by DEC-018 and must not be translated.
///
/// In en, this message translates to:
/// **'Chanora'**
String get appTitle;
/// Greeting shown on the empty home screen before any server is connected.
///
/// In en, this message translates to:
/// **'Welcome to Chanora'**
String get homeGreeting;
/// Plain-language banner informing testers that this build is not for end-user use. Mirrors README.md's status line.
///
/// In en, this message translates to:
/// **'Chanora is currently in early development. This build is not production-ready.'**
String get homeNotProductionReadyBanner;
/// Label for the primary action that opens the connect-to-server flow.
///
/// In en, this message translates to:
/// **'Connect to a server'**
String get connectAction;
}
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
const _AppL10nDelegate();
@override
Future<AppL10n> load(Locale locale) {
return SynchronousFuture<AppL10n>(lookupAppL10n(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'zh'].contains(locale.languageCode);
@override
bool shouldReload(_AppL10nDelegate old) => false;
}
AppL10n lookupAppL10n(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppL10nEn();
case 'zh':
return AppL10nZh();
}
throw FlutterError(
'AppL10n.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}
@@ -0,0 +1,23 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppL10nEn extends AppL10n {
AppL10nEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'Chanora';
@override
String get homeGreeting => 'Welcome to Chanora';
@override
String get homeNotProductionReadyBanner =>
'Chanora is currently in early development. This build is not production-ready.';
@override
String get connectAction => 'Connect to a server';
}
@@ -0,0 +1,23 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppL10nZh extends AppL10n {
AppL10nZh([String locale = 'zh']) : super(locale);
@override
String get appTitle => 'Chanora';
@override
String get homeGreeting => '欢迎使用 Chanora';
@override
String get homeNotProductionReadyBanner =>
'Chanora 目前处于早期开发阶段,此版本尚未达到正式发布质量。';
@override
String get connectAction => '连接到服务器';
}
+72
View File
@@ -0,0 +1,72 @@
// Chanora Flutter application entry point.
//
// Scaffold only. This file is the minimum viable wiring to demonstrate
// the i18n architecture (DEC-015: English + Chinese Simplified) and to
// give `flutter analyze` and `flutter build linux --debug` something
// to compile. Real product UI lands later when the Chanora Design
// System is implemented per docs/ui-ux/.
import 'package:flutter/material.dart';
import 'l10n/generated/app_localizations.dart';
void main() {
runApp(const ChanoraApp());
}
class ChanoraApp extends StatelessWidget {
const ChanoraApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (ctx) => AppL10n.of(ctx).appTitle,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF3F51B5),
),
// DEC-015 (register v0.9.5): English + Chinese Simplified at MVP.
// i18n-ready architecture; additional locales add an ARB file
// under lib/l10n and a Locale entry here.
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: const _HomeScreen(),
);
}
}
class _HomeScreen extends StatelessWidget {
const _HomeScreen();
@override
Widget build(BuildContext context) {
final l10n = AppL10n.of(context);
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.homeGreeting, style: theme.textTheme.headlineSmall),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Text(l10n.homeNotProductionReadyBanner),
),
const SizedBox(height: 24),
FilledButton(
onPressed: null, // wired in a later phase
child: Text(l10n.connectAction),
),
],
),
),
);
}
}