@@ -953,6 +953,348 @@ fn notifyTelegramMsg(allocator: Allocator, msg: []const u8) void {
953953 allocator .free (result .stderr );
954954}
955955
956+ // ═══════════════════════════════════════════════════════════════════════════════
957+ // JUNK MONITOR — tracks untracked files and archive state
958+ // ═══════════════════════════════════════════════════════════════════════════════
959+
960+ const JUNK_ARCHIVE_DIR = "archive/junk-2026-03-15" ;
961+
962+ const JUNK_WHITELIST = [_ ][]const u8 {
963+ "archive/" ,
964+ "papers/trinity-fpga/" ,
965+ "reports/" ,
966+ "src/hslm/" ,
967+ "src/tri/" ,
968+ ".github/" ,
969+ ".trinity/" ,
970+ "deploy/" ,
971+ "fpga/tools/uart_measure.zig" ,
972+ };
973+
974+ pub fn runJunk (allocator : Allocator ) ! void {
975+ std .debug .print ("\n {s}{s}TRINITY DOCTOR \xe2\x80\x94 JUNK MONITOR{s}\n\n " , .{ BOLD , CYAN , RESET });
976+
977+ // 1. Check archive
978+ const archive_exists = blk : {
979+ std .fs .cwd ().access (JUNK_ARCHIVE_DIR , .{}) catch break :blk false ;
980+ break :blk true ;
981+ };
982+
983+ if (archive_exists ) {
984+ std .debug .print (" {s}\xe2\x9c\x93 {s} Archive: {s}\n " , .{ GREEN , RESET , JUNK_ARCHIVE_DIR });
985+ printArchiveStats (allocator );
986+ } else {
987+ std .debug .print (" {s}\xe2\x9c\x97 {s} Archive not found: {s}\n " , .{ RED , RESET , JUNK_ARCHIVE_DIR });
988+ }
989+
990+ // 2. Untracked files via git
991+ std .debug .print ("\n {s}Untracked files:{s}\n " , .{ CYAN , RESET });
992+ const result = std .process .Child .run (.{
993+ .allocator = allocator ,
994+ .argv = &.{ "git" , "status" , "--porcelain" , "-s" },
995+ .max_output_bytes = 65536 ,
996+ }) catch {
997+ std .debug .print (" {s}Cannot run git status{s}\n " , .{ RED , RESET });
998+ return ;
999+ };
1000+ defer allocator .free (result .stdout );
1001+ defer allocator .free (result .stderr );
1002+
1003+ var untracked_total : u32 = 0 ;
1004+ var junk_count : u32 = 0 ;
1005+ var junk_paths : [64 ][256 ]u8 = undefined ;
1006+ var junk_lens : [64 ]usize = [_ ]usize {0 } ** 64 ;
1007+
1008+ var it = std .mem .splitScalar (u8 , result .stdout , '\n ' );
1009+ while (it .next ()) | line | {
1010+ if (line .len < 4 ) continue ;
1011+ if (! std .mem .startsWith (u8 , line , "??" )) continue ;
1012+ const path = std .mem .trimLeft (u8 , line [3.. ], " " );
1013+ untracked_total += 1 ;
1014+
1015+ var whitelisted = false ;
1016+ for (JUNK_WHITELIST ) | wl | {
1017+ if (std .mem .startsWith (u8 , path , wl )) {
1018+ whitelisted = true ;
1019+ break ;
1020+ }
1021+ }
1022+ if (! whitelisted ) {
1023+ if (junk_count < 64 ) {
1024+ const len = @min (path .len , 256 );
1025+ @memcpy (junk_paths [junk_count ][0.. len ], path [0.. len ]);
1026+ junk_lens [junk_count ] = len ;
1027+ }
1028+ junk_count += 1 ;
1029+ }
1030+ }
1031+
1032+ const whitelisted_count = untracked_total - junk_count ;
1033+ std .debug .print (" Total untracked: {d}\n " , .{untracked_total });
1034+ std .debug .print (" Whitelisted: {s}{d}{s} (expected)\n " , .{ GREEN , whitelisted_count , RESET });
1035+
1036+ if (junk_count == 0 ) {
1037+ std .debug .print (" New junk: {s}0{s} \xe2\x9c\x93 \n " , .{ GREEN , RESET });
1038+ std .debug .print ("\n {s}CLEAN{s} \xe2\x80\x94 no new junk files detected\n\n " , .{ GREEN , RESET });
1039+ } else {
1040+ std .debug .print (" New junk: {s}{d}{s} \xe2\x9a\xa0 \n " , .{ YELLOW , junk_count , RESET });
1041+ std .debug .print ("\n {s}New junk files:{s}\n " , .{ YELLOW , RESET });
1042+ for (0.. @min (junk_count , 64 )) | i | {
1043+ std .debug .print (" \xe2\x80\xa2 {s}\n " , .{junk_paths [i ][0.. junk_lens [i ]]});
1044+ }
1045+ if (junk_count > 64 ) {
1046+ std .debug .print (" ... and {d} more\n " , .{junk_count - 64 });
1047+ }
1048+ std .debug .print ("\n Run: mv <files> {s}/misc/\n\n " , .{JUNK_ARCHIVE_DIR });
1049+ }
1050+ }
1051+
1052+ fn printArchiveStats (allocator : Allocator ) void {
1053+ const subdirs = [_ ][]const u8 {
1054+ "fpga-mem-weights" ,
1055+ "fpga-nested-duplicates" ,
1056+ "fpga-synth-reports" ,
1057+ "fpga-test-binaries" ,
1058+ "fpga-misc" ,
1059+ "fpga-weights" ,
1060+ "claude-bak" ,
1061+ "checkpoints-smoke" ,
1062+ "root-test-files" ,
1063+ "misc" ,
1064+ };
1065+ for (subdirs ) | subdir | {
1066+ var path_buf : [256 ]u8 = undefined ;
1067+ const full = std .fmt .bufPrint (& path_buf , "{s}/{s}" , .{ JUNK_ARCHIVE_DIR , subdir }) catch continue ;
1068+
1069+ var count : u32 = 0 ;
1070+ var dir = std .fs .cwd ().openDir (full , .{ .iterate = true }) catch continue ;
1071+ defer dir .close ();
1072+
1073+ var dir_iter = dir .iterate ();
1074+ while (dir_iter .next () catch null ) | _ | {
1075+ count += 1 ;
1076+ }
1077+ if (count > 0 ) {
1078+ std .debug .print (" {s}: {d} files\n " , .{ subdir , count });
1079+ }
1080+ }
1081+ _ = allocator ;
1082+ }
1083+
1084+ // ═══════════════════════════════════════════════════════════════════════════════
1085+ // DOCS MONITOR — checks documentation freshness and data accuracy
1086+ // ═══════════════════════════════════════════════════════════════════════════════
1087+
1088+ const DocsCheck = struct {
1089+ name : []const u8 ,
1090+ passed : bool ,
1091+ detail : []const u8 ,
1092+ };
1093+
1094+ pub fn runDocs (allocator : Allocator ) ! void {
1095+ std .debug .print ("\n {s}{s}TRINITY DOCTOR \xe2\x80\x94 DOCS MONITOR{s}\n\n " , .{ BOLD , CYAN , RESET });
1096+
1097+ var checks_buf : [16 ]DocsCheck = undefined ;
1098+ var check_count : usize = 0 ;
1099+
1100+ // 1. Check docs/ directory exists
1101+ const docs_exists = blk : {
1102+ std .fs .cwd ().access ("docs/docusaurus.config.ts" , .{}) catch break :blk false ;
1103+ break :blk true ;
1104+ };
1105+ checks_buf [check_count ] = .{
1106+ .name = "docs/ directory" ,
1107+ .passed = docs_exists ,
1108+ .detail = if (docs_exists ) "docusaurus.config.ts found" else "MISSING docs/docusaurus.config.ts" ,
1109+ };
1110+ check_count += 1 ;
1111+
1112+ if (! docs_exists ) {
1113+ printDocsReport (checks_buf [0.. check_count ]);
1114+ return ;
1115+ }
1116+
1117+ // 2. Check docs build (node_modules present)
1118+ const nm_exists = blk : {
1119+ std .fs .cwd ().access ("docs/node_modules/.package-lock.json" , .{}) catch break :blk false ;
1120+ break :blk true ;
1121+ };
1122+ checks_buf [check_count ] = .{
1123+ .name = "node_modules" ,
1124+ .passed = nm_exists ,
1125+ .detail = if (nm_exists ) "installed" else "MISSING \xe2\x80\x94 run: cd docs && npm install" ,
1126+ };
1127+ check_count += 1 ;
1128+
1129+ // 3. Compare source freshness: README.md vs docs/docs/intro.md
1130+ const readme_ts = getFileMtime ("README.md" );
1131+ const intro_ts = getFileMtime ("docs/docs/intro.md" );
1132+ const readme_fresh = intro_ts >= readme_ts or readme_ts == 0 ;
1133+ checks_buf [check_count ] = .{
1134+ .name = "intro.md freshness" ,
1135+ .passed = readme_fresh ,
1136+ .detail = if (readme_fresh ) "up to date with README.md" else "STALE \xe2\x80\x94 README.md newer than docs/docs/intro.md" ,
1137+ };
1138+ check_count += 1 ;
1139+
1140+ // 4. Check CLI docs count vs actual tri commands
1141+ const cli_doc_count = countFilesInDir ("docs/docs/cli" );
1142+ const cli_ok = cli_doc_count >= 30 ;
1143+ checks_buf [check_count ] = .{
1144+ .name = "CLI docs coverage" ,
1145+ .passed = cli_ok ,
1146+ .detail = if (cli_ok ) "adequate coverage" else "LOW \xe2\x80\x94 less than 30 CLI doc pages" ,
1147+ };
1148+ check_count += 1 ;
1149+
1150+ // 5. Check benchmarks freshness vs EXPERIENCE_LOG.md
1151+ const exp_ts = getFileMtime ("EXPERIENCE_LOG.md" );
1152+ const bench_ts = getFileMtime ("docs/docs/benchmarks/index.md" );
1153+ const bench_fresh = bench_ts >= exp_ts or exp_ts == 0 or bench_ts == 0 ;
1154+ checks_buf [check_count ] = .{
1155+ .name = "benchmarks freshness" ,
1156+ .passed = bench_fresh ,
1157+ .detail = if (bench_fresh ) "up to date" else "STALE \xe2\x80\x94 EXPERIENCE_LOG.md newer than benchmarks" ,
1158+ };
1159+ check_count += 1 ;
1160+
1161+ // 6. Check FPGA docs vs synthesis results
1162+ const fpga_src_ts = getFileMtime ("fpga/openxc7-synth/hslm_full_top.bit" );
1163+ const fpga_doc_ts = getFileMtime ("docs/docs/fpga/TECHNOLOGY_TREE_ACTION_PLAN.md" );
1164+ const fpga_fresh = fpga_doc_ts >= fpga_src_ts or fpga_src_ts == 0 or fpga_doc_ts == 0 ;
1165+ checks_buf [check_count ] = .{
1166+ .name = "FPGA docs freshness" ,
1167+ .passed = fpga_fresh ,
1168+ .detail = if (fpga_fresh ) "up to date" else "STALE \xe2\x80\x94 bitstream newer than FPGA docs" ,
1169+ };
1170+ check_count += 1 ;
1171+
1172+ // 7. Check key data in intro.md matches README
1173+ const data_match = checkIntroData (allocator );
1174+ checks_buf [check_count ] = .{
1175+ .name = "intro.md data accuracy" ,
1176+ .passed = data_match ,
1177+ .detail = if (data_match ) "key numbers match README" else "MISMATCH \xe2\x80\x94 intro.md numbers differ from README.md" ,
1178+ };
1179+ check_count += 1 ;
1180+
1181+ // 8. Check API docs coverage
1182+ const api_count = countFilesInDir ("docs/docs/api" );
1183+ const api_ok = api_count >= 10 ;
1184+ checks_buf [check_count ] = .{
1185+ .name = "API docs coverage" ,
1186+ .passed = api_ok ,
1187+ .detail = if (api_ok ) "adequate coverage" else "LOW \xe2\x80\x94 less than 10 API doc pages" ,
1188+ };
1189+ check_count += 1 ;
1190+
1191+ // 9. Try docs build (quick dry-run check)
1192+ std .debug .print (" Building docs (dry run)... " , .{});
1193+ const build_ok = tryDocsBuild (allocator );
1194+ checks_buf [check_count ] = .{
1195+ .name = "docs build" ,
1196+ .passed = build_ok ,
1197+ .detail = if (build_ok ) "builds successfully" else "BROKEN \xe2\x80\x94 cd docs && npm run build fails" ,
1198+ };
1199+ check_count += 1 ;
1200+
1201+ printDocsReport (checks_buf [0.. check_count ]);
1202+ }
1203+
1204+ fn getFileMtime (path : []const u8 ) i128 {
1205+ const file = std .fs .cwd ().openFile (path , .{}) catch return 0 ;
1206+ defer file .close ();
1207+ const stat = file .stat () catch return 0 ;
1208+ return stat .mtime ;
1209+ }
1210+
1211+ fn countFilesInDir (dir_path : []const u8 ) u32 {
1212+ var count : u32 = 0 ;
1213+ var dir = std .fs .cwd ().openDir (dir_path , .{ .iterate = true }) catch return 0 ;
1214+ defer dir .close ();
1215+ var it = dir .iterate ();
1216+ while (it .next () catch null ) | entry | {
1217+ if (entry .kind == .file and std .mem .endsWith (u8 , entry .name , ".md" )) {
1218+ count += 1 ;
1219+ }
1220+ }
1221+ return count ;
1222+ }
1223+
1224+ fn checkIntroData (allocator : Allocator ) bool {
1225+ // Check that key numbers from README appear in intro.md
1226+ const readme = std .fs .cwd ().readFileAlloc (allocator , "README.md" , 65536 ) catch return true ;
1227+ defer allocator .free (readme );
1228+ const intro = std .fs .cwd ().readFileAlloc (allocator , "docs/docs/intro.md" , 65536 ) catch return true ;
1229+ defer allocator .free (intro );
1230+
1231+ // Check for key markers that should be in both
1232+ const markers = [_ ][]const u8 {
1233+ "1.58 bits" ,
1234+ "20x" ,
1235+ "Trinity" ,
1236+ };
1237+ for (markers ) | m | {
1238+ if (std .mem .indexOf (u8 , readme , m ) != null ) {
1239+ if (std .mem .indexOf (u8 , intro , m ) == null ) return false ;
1240+ }
1241+ }
1242+ return true ;
1243+ }
1244+
1245+ fn tryDocsBuild (allocator : Allocator ) bool {
1246+ const result = std .process .Child .run (.{
1247+ .allocator = allocator ,
1248+ .argv = &.{ "npm" , "run" , "build" },
1249+ .cwd = "docs" ,
1250+ .max_output_bytes = 65536 ,
1251+ }) catch {
1252+ std .debug .print ("{s}FAIL{s}\n " , .{ RED , RESET });
1253+ return false ;
1254+ };
1255+ defer allocator .free (result .stdout );
1256+ defer allocator .free (result .stderr );
1257+
1258+ const exit = switch (result .term ) {
1259+ .Exited = > | code | code ,
1260+ else = > 1 ,
1261+ };
1262+ if (exit == 0 ) {
1263+ std .debug .print ("{s}OK{s}\n " , .{ GREEN , RESET });
1264+ } else {
1265+ std .debug .print ("{s}FAIL{s}\n " , .{ RED , RESET });
1266+ }
1267+ return exit == 0 ;
1268+ }
1269+
1270+ fn printDocsReport (checks : []const DocsCheck ) void {
1271+ var passed : u32 = 0 ;
1272+ var total : u32 = 0 ;
1273+
1274+ std .debug .print ("\n {s}Documentation checks:{s}\n " , .{ CYAN , RESET });
1275+ for (checks ) | c | {
1276+ total += 1 ;
1277+ if (c .passed ) {
1278+ passed += 1 ;
1279+ std .debug .print (" {s}\xe2\x9c\x93 {s} {s}: {s}\n " , .{ GREEN , RESET , c .name , c .detail });
1280+ } else {
1281+ std .debug .print (" {s}\xe2\x9c\x97 {s} {s}: {s}\n " , .{ RED , RESET , c .name , c .detail });
1282+ }
1283+ }
1284+
1285+ std .debug .print ("\n Score: {s}{d}/{d}{s}" , .{
1286+ if (passed == total ) GREEN else if (passed * 2 >= total ) YELLOW else RED ,
1287+ passed ,
1288+ total ,
1289+ RESET ,
1290+ });
1291+ if (passed == total ) {
1292+ std .debug .print (" \xe2\x80\x94 all docs checks pass \xe2\x9c\x93 \n\n " , .{});
1293+ } else {
1294+ std .debug .print (" \xe2\x80\x94 {d} issues found, run /doctor docs to fix\n\n " , .{total - passed });
1295+ }
1296+ }
1297+
9561298// ═══════════════════════════════════════════════════════════════════════════════
9571299// TESTS
9581300// ═══════════════════════════════════════════════════════════════════════════════
0 commit comments