11use serde:: Serialize ;
2+ use sha2:: { Digest , Sha256 } ;
23use std:: sync:: atomic:: AtomicBool ;
34use std:: sync:: Arc ;
45use tauri:: { Emitter , State } ;
@@ -116,6 +117,16 @@ If the user seems stuck or unsure what to discuss next, suggest the next uncover
116117- Propagate typos or unclear terms without clarifying
117118- Rush to architecture before understanding the problem"## ;
118119
120+ const EXPORT_FILE_ORDER : & [ & str ] = & [
121+ "START_HERE.md" ,
122+ "README.md" ,
123+ "SPEC.md" ,
124+ "CLAUDE.md" ,
125+ "PROMPTS.md" ,
126+ "MODEL_HANDOFF.md" ,
127+ "CONVERSATION.md" ,
128+ ] ;
129+
119130fn to_response < E : Into < AppError > > ( err : E ) -> ErrorResponse {
120131 err. into ( ) . to_response ( )
121132}
@@ -1072,6 +1083,7 @@ pub async fn save_to_folder(
10721083 }
10731084
10741085 let manifest = ExportManifest {
1086+ schema_version : 2 ,
10751087 session_id : session_id_for_thread. clone ( ) ,
10761088 session_name : session_name_for_thread. clone ( ) ,
10771089 target : meta_for_thread
@@ -1096,10 +1108,7 @@ pub async fn save_to_folder(
10961108 . and_then ( |m| m. confidence_json . as_ref ( ) )
10971109 . and_then ( |q| serde_json:: from_str :: < ConfidenceReport > ( q) . ok ( ) ) ,
10981110 import_context : import_context_for_thread. clone ( ) ,
1099- files : docs_for_thread
1100- . iter ( )
1101- . map ( |doc| doc. filename . clone ( ) )
1102- . collect ( ) ,
1111+ files : build_export_manifest_files ( & docs_for_thread) ,
11031112 } ;
11041113 let manifest_json =
11051114 serde_json:: to_string_pretty ( & manifest) . map_err ( |e| AppError :: FileSystem {
@@ -1229,6 +1238,7 @@ fn resolve_forge_target(
12291238
12301239#[ derive( Debug , Clone , Serialize ) ]
12311240struct ExportManifest {
1241+ schema_version : u32 ,
12321242 session_id : String ,
12331243 session_name : String ,
12341244 target : String ,
@@ -1238,11 +1248,59 @@ struct ExportManifest {
12381248 quality : Option < QualityReport > ,
12391249 confidence : Option < ConfidenceReport > ,
12401250 import_context : Option < CodebaseImportSummary > ,
1241- files : Vec < String > ,
1251+ files : Vec < ExportManifestFile > ,
1252+ }
1253+
1254+ #[ derive( Debug , Clone , Serialize ) ]
1255+ struct ExportManifestFile {
1256+ filename : String ,
1257+ bytes : usize ,
1258+ lines : usize ,
1259+ sha256 : String ,
12421260}
12431261
12441262// ============ HELPERS ============
12451263
1264+ fn build_export_manifest_files ( docs : & [ GeneratedDocument ] ) -> Vec < ExportManifestFile > {
1265+ let mut files: Vec < ExportManifestFile > = docs
1266+ . iter ( )
1267+ . map ( |doc| ExportManifestFile {
1268+ filename : doc. filename . clone ( ) ,
1269+ bytes : doc. content . len ( ) ,
1270+ lines : if doc. content . is_empty ( ) {
1271+ 0
1272+ } else {
1273+ doc. content . lines ( ) . count ( )
1274+ } ,
1275+ sha256 : sha256_hex ( doc. content . as_bytes ( ) ) ,
1276+ } )
1277+ . collect ( ) ;
1278+
1279+ files. sort_by ( |a, b| {
1280+ let rank_a = export_file_rank ( & a. filename ) ;
1281+ let rank_b = export_file_rank ( & b. filename ) ;
1282+ rank_a
1283+ . cmp ( & rank_b)
1284+ . then_with ( || a. filename . cmp ( & b. filename ) )
1285+ } ) ;
1286+
1287+ files
1288+ }
1289+
1290+ fn export_file_rank ( filename : & str ) -> usize {
1291+ EXPORT_FILE_ORDER
1292+ . iter ( )
1293+ . position ( |known| known == & filename)
1294+ . unwrap_or ( EXPORT_FILE_ORDER . len ( ) )
1295+ }
1296+
1297+ fn sha256_hex ( bytes : & [ u8 ] ) -> String {
1298+ let mut hasher = Sha256 :: new ( ) ;
1299+ hasher. update ( bytes) ;
1300+ let digest = hasher. finalize ( ) ;
1301+ digest. iter ( ) . map ( |b| format ! ( "{:02x}" , b) ) . collect ( )
1302+ }
1303+
12461304fn extract_import_summary_from_metadata ( metadata : & str ) -> Option < CodebaseImportSummary > {
12471305 let value = serde_json:: from_str :: < serde_json:: Value > ( metadata) . ok ( ) ?;
12481306 serde_json:: from_value :: < CodebaseImportSummary > ( value. get ( "import_summary" ) ?. clone ( ) ) . ok ( )
@@ -1271,3 +1329,65 @@ fn build_search_context(query: &str, results: &[SearchResult]) -> String {
12711329
12721330 context
12731331}
1332+
1333+ #[ cfg( test) ]
1334+ mod tests {
1335+ use super :: * ;
1336+
1337+ fn doc ( filename : & str , content : & str ) -> GeneratedDocument {
1338+ GeneratedDocument {
1339+ id : "doc-id" . to_string ( ) ,
1340+ session_id : "session-id" . to_string ( ) ,
1341+ filename : filename. to_string ( ) ,
1342+ content : content. to_string ( ) ,
1343+ created_at : "2026-01-01 00:00:00" . to_string ( ) ,
1344+ }
1345+ }
1346+
1347+ #[ test]
1348+ fn build_export_manifest_files_orders_known_documents_first ( ) {
1349+ let files = build_export_manifest_files ( & [
1350+ doc ( "Z_NOTES.md" , "notes" ) ,
1351+ doc ( "README.md" , "read me" ) ,
1352+ doc ( "START_HERE.md" , "start here" ) ,
1353+ doc ( "A_CUSTOM.md" , "custom" ) ,
1354+ ] ) ;
1355+
1356+ let ordered_names: Vec < String > = files. into_iter ( ) . map ( |f| f. filename ) . collect ( ) ;
1357+ assert_eq ! (
1358+ ordered_names,
1359+ vec![
1360+ "START_HERE.md" . to_string( ) ,
1361+ "README.md" . to_string( ) ,
1362+ "A_CUSTOM.md" . to_string( ) ,
1363+ "Z_NOTES.md" . to_string( ) ,
1364+ ]
1365+ ) ;
1366+ }
1367+
1368+ #[ test]
1369+ fn build_export_manifest_files_includes_hash_bytes_and_lines ( ) {
1370+ let files = build_export_manifest_files ( & [ doc ( "SPEC.md" , "abc" ) , doc ( "EMPTY.md" , "" ) ] ) ;
1371+ let spec = files
1372+ . iter ( )
1373+ . find ( |f| f. filename == "SPEC.md" )
1374+ . expect ( "SPEC.md entry missing" ) ;
1375+ assert_eq ! ( spec. bytes, 3 ) ;
1376+ assert_eq ! ( spec. lines, 1 ) ;
1377+ assert_eq ! (
1378+ spec. sha256,
1379+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
1380+ ) ;
1381+
1382+ let empty = files
1383+ . iter ( )
1384+ . find ( |f| f. filename == "EMPTY.md" )
1385+ . expect ( "EMPTY.md entry missing" ) ;
1386+ assert_eq ! ( empty. bytes, 0 ) ;
1387+ assert_eq ! ( empty. lines, 0 ) ;
1388+ assert_eq ! (
1389+ empty. sha256,
1390+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
1391+ ) ;
1392+ }
1393+ }
0 commit comments