22/** Continuously regenerate the Dirac Architecture Optimization Twin on source changes. */
33import fs from 'node:fs' ;
44import path from 'node:path' ;
5- import { spawn } from 'node:child_process' ;
5+ import { createHash } from 'node:crypto' ;
6+ import { execFileSync , spawn } from 'node:child_process' ;
67
78const ROOT = path . resolve ( import . meta. dirname , '..' ) ;
89const scope = JSON . parse ( fs . readFileSync ( path . join ( ROOT , 'scripts/digital_twin_scope.json' ) , 'utf8' ) ) ;
910const rootFiles = new Set ( scope . include_root_files ) ;
1011const roots = scope . include_roots . map ( x => x . replace ( / \/ $ / , '' ) ) ;
1112const externalRoots = scope . external_roots . map ( x => x . replace ( / \/ $ / , '' ) ) ;
1213const outputs = new Set ( scope . generated_outputs ) ;
13- const watched = new Map ( ) ;
1414let timer ;
1515let running = false ;
1616let rerun = false ;
@@ -59,47 +59,31 @@ function schedule(reason) {
5959 timer = setTimeout ( ( ) => build ( reason ) , 900 ) ;
6060}
6161
62- function observed ( base , filename ) {
63- if ( ! filename ) return ;
64- const rel = normalized ( path . relative ( ROOT , path . join ( base , filename . toString ( ) ) ) ) ;
65- if ( ! inScope ( rel ) ) return ;
66- changed . add ( rel ) ;
67- schedule ( 'source change' ) ;
68- }
69-
70- function watchDirectory ( absolute ) {
71- if ( watched . has ( absolute ) || ! fs . existsSync ( absolute ) ) return ;
72- const watcher = fs . watch ( absolute , { recursive : true } , ( _event , filename ) => observed ( absolute , filename ) ) ;
73- watcher . on ( 'error' , ( ) => {
74- watcher . close ( ) ;
75- watched . delete ( absolute ) ;
62+ function sourceFiles ( ) {
63+ const raw = execFileSync ( 'git' , [ 'ls-files' , '--cached' , '--others' , '--exclude-standard' , '-z' ] , {
64+ cwd : ROOT , encoding : 'buffer' , maxBuffer : 32 * 1024 * 1024 ,
7665 } ) ;
77- watched . set ( absolute , watcher ) ;
66+ return raw . toString ( ) . split ( '\0' ) . filter ( Boolean ) . map ( normalized ) . filter ( inScope ) . sort ( ) ;
7867}
7968
80- function watchChildren ( parent , parentRel ) {
81- const key = `${ parent } :children` ;
82- if ( watched . has ( key ) || ! fs . existsSync ( parent ) ) return ;
83- for ( const entry of fs . readdirSync ( parent , { withFileTypes : true } ) ) {
84- if ( ! entry . isDirectory ( ) ) continue ;
85- const rel = normalized ( `${ parentRel } /${ entry . name } ` ) ;
86- if ( ! externalRoots . some ( root => rel === root || rel . startsWith ( `${ root } /` ) ) ) {
87- watchDirectory ( path . join ( parent , entry . name ) ) ;
69+ function sourceState ( ) {
70+ const state = new Map ( ) ;
71+ for ( const rel of sourceFiles ( ) ) {
72+ try {
73+ const absolute = path . join ( ROOT , rel ) ;
74+ if ( ! fs . statSync ( absolute ) . isFile ( ) ) continue ;
75+ const digest = createHash ( 'sha256' ) . update ( fs . readFileSync ( absolute ) ) . digest ( 'hex' ) ;
76+ state . set ( rel , digest ) ;
77+ } catch ( error ) {
78+ if ( error ?. code !== 'ENOENT' ) throw error ;
8879 }
8980 }
90- const watcher = fs . watch ( parent , ( _event , filename ) => {
91- if ( ! filename ) return ;
92- const name = filename . toString ( ) ;
93- const rel = normalized ( `${ parentRel } /${ name } ` ) ;
94- const absolute = path . join ( parent , name ) ;
95- const external = externalRoots . some ( root => rel === root || rel . startsWith ( `${ root } /` ) ) ;
96- if ( fs . existsSync ( absolute ) && fs . statSync ( absolute ) . isDirectory ( ) && ! external ) {
97- watchDirectory ( absolute ) ;
98- } else if ( inScope ( rel ) ) {
99- observed ( parent , filename ) ;
100- }
101- } ) ;
102- watched . set ( key , watcher ) ;
81+ return state ;
82+ }
83+
84+ function changedFiles ( previous , current ) {
85+ const paths = new Set ( [ ...previous . keys ( ) , ...current . keys ( ) ] ) ;
86+ return [ ...paths ] . filter ( rel => previous . get ( rel ) !== current . get ( rel ) ) . sort ( ) ;
10387}
10488
10589if ( process . argv . includes ( '--selftest' ) ) {
@@ -111,37 +95,28 @@ if (process.argv.includes('--selftest')) {
11195 process . stderr . write ( `${ JSON . stringify ( verdicts ) } \n` ) ;
11296 process . exit ( 1 ) ;
11397 }
114- process . stdout . write ( `watch scope OK · ${ roots . length } first-party roots · upstream and generated outputs excluded\n` ) ;
98+ const discovered = sourceState ( ) . size ;
99+ if ( ! discovered ) throw new Error ( 'source poller discovered no first-party files' ) ;
100+ process . stdout . write ( `watch scope OK · ${ roots . length } first-party roots · ${ discovered } files polled · upstream and generated outputs excluded\n` ) ;
115101 process . exit ( 0 ) ;
116102}
117103
118104if ( process . argv . includes ( '--once' ) ) {
119105 build ( 'one-shot synchronization' ) ;
120106} else {
107+ let baseline = sourceState ( ) ;
121108 build ( ) ;
122- for ( const root of roots ) watchDirectory ( path . join ( ROOT , root ) ) ;
123- for ( const entry of fs . readdirSync ( ROOT , { withFileTypes : true } ) ) {
124- if ( ! entry . isDirectory ( ) ) continue ;
125- if ( entry . name === 'src' ) {
126- watchChildren ( path . join ( ROOT , 'src' ) , 'src' ) ;
127- continue ;
128- }
129- if ( externalRoots . includes ( entry . name ) ) continue ;
130- watchDirectory ( path . join ( ROOT , entry . name ) ) ;
131- }
132- fs . watch ( ROOT , ( _event , filename ) => {
133- if ( ! filename ) return ;
134- const name = filename . toString ( ) ;
135- const absolute = path . join ( ROOT , name ) ;
136- if ( ! fs . existsSync ( absolute ) && watched . has ( absolute ) ) {
137- watched . get ( absolute ) . close ( ) ;
138- watched . delete ( absolute ) ;
109+ setInterval ( ( ) => {
110+ try {
111+ const current = sourceState ( ) ;
112+ const updates = changedFiles ( baseline , current ) ;
113+ baseline = current ;
114+ if ( ! updates . length ) return ;
115+ for ( const rel of updates ) changed . add ( rel ) ;
116+ schedule ( 'source change' ) ;
117+ } catch ( error ) {
118+ process . stderr . write ( `[${ new Date ( ) . toISOString ( ) } ] source poll failed · ${ error . message } \n` ) ;
139119 }
140- if ( fs . existsSync ( absolute ) && fs . statSync ( absolute ) . isDirectory ( )
141- && ! externalRoots . some ( root => name === root || root . startsWith ( `${ name } /` ) ) ) {
142- watchDirectory ( absolute ) ;
143- }
144- if ( rootFiles . has ( name ) || inScope ( name ) ) observed ( ROOT , filename ) ;
145- } ) ;
146- process . stdout . write ( `watching ${ roots . length } first-party roots · debounce 900 ms · runtime captured on rebuild\n` ) ;
120+ } , 2_000 ) ;
121+ process . stdout . write ( `polling ${ baseline . size } first-party files · interval 2 s · debounce 900 ms · runtime captured on rebuild\n` ) ;
147122}
0 commit comments