promote diff now does diff instead of timestamps
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 10 Mar 2026 12:55:03 +0000 (12:55 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 10 Mar 2026 12:55:03 +0000 (12:55 +0000)
developer/document/C_single_file_modules_and_namespaces.html [deleted file]
developer/document/File_directory_naming.html
developer/document/Single-file_C_modules_and_namespaces.html [new file with mode: 0644]
developer/tool/promote

diff --git a/developer/document/C_single_file_modules_and_namespaces.html b/developer/document/C_single_file_modules_and_namespaces.html
deleted file mode 100644 (file)
index 18284b2..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8">
-    <title>C single file modules and namespaces</title>
-    <script src="setup.js"></script>
-    <script>
-      window.StyleRT.include('RT/theme');
-      window.StyleRT.include('RT/layout/article_tech_ref');
-    </script>
-  </head>
-  <body>
-    <RT-article>
-      <RT-title 
-        author="Thomas Walker Lynch" 
-        date="2026-03-10" 
-        title="C single file modules and namespaces">
-      </RT-title>
-
-      <RT-TOC level="1"></RT-TOC>
-
-      <h1>Single file C source</h1>
-      <p>
-        The RT C-culture abandons the traditional separation of declarations into <RT-code>.h</RT-code> header files and definitions into <RT-code>.c</RT-code> source files. Instead, a developer integrates the interface and the implementation into a single file. 
-      </p>
-      <p>
-        When a person includes the file using an <RT-code>#include</RT-code> directive, the file exposes only its interface. When the build system compiles the file into an object, a preprocessor macro acts as an implementation gate to compile the definitions. This keeps the source tree clean and ensures the interface and implementation never fall out of synchronization.
-      </p>
-
-      <h1>Ad hoc namespaces</h1>
-      <p>
-        The C language lacks native namespaces. To prevent symbol collisions in large projects, RT code format uses a center dot (<RT-code>·</RT-code>) to denote ad hoc namespaces within identifiers. 
-      </p>
-      <p>
-        A programmer maps the directory structure directly to these namespaces. For example, a file located at <RT-code>authored/ExampleGreet/Math.lib.c</RT-code> belongs to the <RT-code>ExampleGreet</RT-code> namespace and defines the <RT-code>Math</RT-code> module. 
-      </p>
-      <p>
-        Types and modules use <RT-code>PascalCase</RT-code>, while functions and variables use <RT-code>snake_case</RT-code>. An exported function from this module carries the full namespace and module prefix, such as <RT-code>ExampleGreet·Math·add</RT-code>.
-      </p>
-
-      <h1>The implementation gate</h1>
-      <p>
-        To achieve the single-file source pattern, the code relies on two preprocessor constructs. The entire file is wrapped in an include guard using the <RT-code>·ONCE</RT-code> suffix. This prevents redeclaration errors if the file is included multiple times.
-      </p>
-      <p>
-        The definitions are wrapped in a single <RT-code>#ifdef</RT-code> block using the exact namespace and module name. The build system dynamically injects this macro via a <RT-code>-D</RT-code> compiler flag when building the module's specific object file. 
-      </p>
-      
-      <h2>Example: Math.lib.c</h2>
-      <p>
-        The following example demonstrates the complete structure.
-      </p>
-
-      <RT-code>
-        #ifndef ExampleGreet·Math·ONCE
-        #define ExampleGreet·Math·ONCE
-
-        int ExampleGreet·Math·add(int a ,int b);
-
-        #ifdef ExampleGreet·Math
-
-        int ExampleGreet·Math·add(int a ,int b){
-          return a + b;
-        }
-
-        #endif // ExampleGreet·Math
-        #endif // ExampleGreet·Math·ONCE
-      </RT-code>
-
-      <h2>Build system mechanics</h2>
-      <p>
-        When a consumer file, such as <RT-code>hello.CLI.c</RT-code>, contains <RT-code>#include "Math.lib.c"</RT-code>, the compiler processes the file without the <RT-code>ExampleGreet·Math</RT-code> macro defined. It skips the implementation and reads only the function prototype.
-      </p>
-      <p>
-        When the orchestrator compiles the library object, it evaluates the target name and explicitly passes <RT-code>-DExampleGreet·Math</RT-code> to the compiler. This unlocks the gate, compiling the machine code for the definitions into <RT-code>Math.lib.o</RT-code>.
-      </p>
-
-    </RT-article>
-  </body>
-</html>
index 7569629..2d7a350 100644 (file)
@@ -17,6 +17,8 @@
         title="File and directory naming conventions">
       </RT-title>
 
+      <RT-TOC level="1"></RT-TOC>
+
       <h1>Program file system objects</h1>
 
       <p>
diff --git a/developer/document/Single-file_C_modules_and_namespaces.html b/developer/document/Single-file_C_modules_and_namespaces.html
new file mode 100644 (file)
index 0000000..c05225e
--- /dev/null
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <title>Single-file C modules and namespaces</title>
+    <script src="setup.js"></script>
+    <script>
+      window.StyleRT.include('RT/theme');
+      window.StyleRT.include('RT/layout/article_tech_ref');
+    </script>
+  </head>
+  <body>
+    <RT-article>
+      <RT-title 
+        author="Thomas Walker Lynch" 
+        date="2026-03-10" 
+        title="Single-file C modules and namespaces">
+      </RT-title>
+
+      <RT-TOC level="1"></RT-TOC>
+
+      <h1>Single-file C source</h1>
+      <p>
+        The RT C language culture abandons the traditional separation of declarations into <RT-code>.h</RT-code> header files and definitions into <RT-code>.c</RT-code> source files. A developer integrates the interface and the implementation into a single file. 
+      </p>
+      <p>
+        When a person includes the file using an <RT-code>#include</RT-code> directive, the file exposes only its interface. When the build system compiles the file into an object, a preprocessor macro acts as an implementation gate to compile the definitions. This keeps the source tree clean and ensures the interface and implementation never fall out of synchronization.
+      </p>
+
+      <h1>Ad hoc namespaces</h1>
+      <p>
+        The C language lacks native namespaces. To prevent symbol collisions in large projects, RT code format uses a center dot (<RT-code>·</RT-code>) to denote ad hoc namespaces within identifiers. 
+      </p>
+      <p>
+        A programmer maps the directory structure directly to these namespaces. For example, a file located at <RT-code>authored/ExampleGreet/Math.lib.c</RT-code> belongs to the <RT-code>ExampleGreet</RT-code> namespace and defines the <RT-code>Math</RT-code> module. 
+      </p>
+      <ul>
+        <li>Types and modules use <RT-code>PascalCase</RT-code>.</li>
+        <li>Functions and variables use <RT-code>snake_case</RT-code>.</li>
+      </ul>
+      <p>
+        An exported function from this module carries the full namespace and module prefix, such as <RT-code>ExampleGreet·Math·add</RT-code>.
+      </p>
+
+      <h1>The implementation gate</h1>
+      <p>
+        To achieve the single-file source pattern, the code relies on two preprocessor constructs:
+      </p>
+      <ul>
+        <li><strong>Include Guard:</strong> The entire file is wrapped in an include guard using the <RT-code>·ONCE</RT-code> suffix. This prevents redeclaration errors if the file is included multiple times.</li>
+        <li><strong>Implementation Block:</strong> The definitions are wrapped in a single <RT-code>#ifdef</RT-code> block using the exact namespace and module name. The build system dynamically injects this macro via a <RT-code>-D</RT-code> compiler flag when building the module's specific object file.</li>
+      </ul>
+      
+      <h2>Build system mechanics</h2>
+      <p>
+        When a consumer file, such as <RT-code>hello.CLI.c</RT-code>, contains <RT-code>#include "Math.lib.c"</RT-code>, the compiler processes the file without the <RT-code>ExampleGreet·Math</RT-code> macro defined. It skips the implementation and reads only the function prototype.
+      </p>
+      <p>
+        When the orchestrator compiles the library object, it evaluates the target name and explicitly passes <RT-code>-DExampleGreet·Math</RT-code> to the compiler. This unlocks the gate, compiling the machine code for the definitions into <RT-code>Math.lib.o</RT-code>.
+      </p>
+
+      <h2>Example: Math.lib.c</h2>
+      <p>
+        The following example demonstrates the complete structure.
+      </p>
+
+      <RT-code>
+        #ifndef ExampleGreet·Math·ONCE
+        #define ExampleGreet·Math·ONCE
+
+        int ExampleGreet·Math·add(int a ,int b);
+
+        #ifdef ExampleGreet·Math
+
+        int ExampleGreet·Math·add(int a ,int b){
+          return a + b;
+        }
+
+        #endif // ExampleGreet·Math
+        #endif // ExampleGreet·Math·ONCE
+      </RT-code>
+
+      <h2>Cross-module dependencies</h2>
+      <p>
+        When one module depends on another, the developer directly includes the library source file. For example, if the Greeter module requires the Math module, the file <RT-code>Greeter.lib.c</RT-code> will contain:
+      </p>
+      <RT-code>
+        #include "Math.lib.c"
+      </RT-code>
+      <p>
+        Because every file is protected by a <RT-code>·ONCE</RT-code> include guard, it is safe for multiple modules to include the same dependency. The preprocessor will only expand the interface once per translation unit.
+      </p>
+
+      <h2>Information hiding</h2>
+      <p>
+        To define internal helper functions or private data that should not be exposed in the module's public interface, a programmer places them strictly inside the <RT-code>#ifdef</RT-code> implementation block.
+      </p>
+      <p>
+        To prevent these internal symbols from leaking into the global namespace during linking, they must be given internal linkage. The RT skeleton provides the <RT-code>Local</RT-code> macro (defined as <RT-code>static</RT-code> in <RT-code>RT_global.h</RT-code>) for this exact purpose.
+      </p>
+      <RT-code>
+        #ifdef ExampleGreet·Math
+
+        Local int internal_helper(int val){
+          return val * 2;
+        }
+
+        int ExampleGreet·Math·add(int a ,int b){
+          return internal_helper(a) + b;
+        }
+
+        #endif // ExampleGreet·Math
+      </RT-code>
+
+      <h2>Executable entry points</h2>
+      <p>
+        Programs intended to be compiled into standalone executables use the <RT-code>.CLI.c</RT-code> suffix. These files consume the library modules but do not define their own namespaces or include guards, as they are never included by other files.
+      </p>
+      <p>
+        A <RT-code>.CLI.c</RT-code> file includes the necessary <RT-code>.lib.c</RT-code> files, parses command-line arguments in the <RT-code>main</RT-code> function, and passes native data types to a dedicated <RT-code>CLI()</RT-code> function to orchestrate the core logic.
+      </p>
+
+    </RT-article>
+  </body>
+</html>
index 349bfd7..a4d8888 100755 (executable)
@@ -1,13 +1,13 @@
 #!/usr/bin/env -S python3 -B
 # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
 
-import os ,sys ,shutil ,stat ,pwd ,grp ,glob ,tempfile
+import os ,sys ,shutil ,stat ,pwd ,grp ,glob ,tempfile ,filecmp
 
 HELP = """usage: promote {write|clean|ls|diff|help|dry write}
   write       Writes promoted files from scratchpad/made into consumer/made. Only updates newer files.
   clean       Remove all contents of the consumer/made directory.
   ls          List consumer/made as an indented tree: PERMS  OWNER  NAME.
-  diff        List files in consumer/made that are not in scratchpad/made, or are newer.
+  diff        List missing, orphaned, or out-of-sync files between scratchpad and consumer.
   help        Show this message.
   dry write   Preview what write would do without modifying the filesystem.
 """
@@ -22,7 +22,7 @@ def exit_with_status(msg ,code=1):
 
 def assert_setup():
   setup_val = os.environ.get("SETUP" ,"")
-  if(setup_val != SETUP_MUST_BE):
+  if( setup_val != SETUP_MUST_BE ):
     hint = (
       "SETUP is not 'developer/tool/setup'.\n"
       "Enter the project with:  . setup developer\n"
@@ -81,7 +81,7 @@ def ensure_mode(path_str ,mode):
   except Exception: pass
 
 def ensure_dir(path_str ,mode=DEFAULT_DIR_MODE ,dry=False):
-  if(dry):
+  if( dry ):
     if( not os.path.isdir(path_str) ):
       shown = _display_dst(path_str) if path_str.startswith(consumer_root()) else (
         os.path.relpath(path_str ,dev_root()) if path_str.startswith(dev_root()) else path_str
@@ -109,42 +109,42 @@ def list_tree(root_dp):
       it = list(os.scandir(path_str))
     except FileNotFoundError:
       return
-    dirs = [e for e in it if e.is_dir(follow_symlinks=False)]
-    files = [e for e in it if not e.is_dir(follow_symlinks=False)]
-    dirs.sort(key=lambda e: e.name)
-    files.sort(key=lambda e: e.name)
-
-    if(is_root):
-      for f in ( e for e in files if e.name.startswith(".") ):
-        st = os.lstat(f.path)
-        entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,f.name))
-      for d in dirs:
-        st = os.lstat(d.path)
-        entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,d.name + "/"))
-        gather(d.path ,depth + 1 ,False)
-      for f in ( e for e in files if not e.name.startswith(".") ):
-        st = os.lstat(f.path)
-        entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,f.name))
+    dirs = [TM_e for TM_e in it if TM_e.is_dir(follow_symlinks=False)]
+    files = [TM_e for TM_e in it if not TM_e.is_dir(follow_symlinks=False)]
+    dirs.sort(key=lambda TM_e: TM_e.name)
+    files.sort(key=lambda TM_e: TM_e.name)
+
+    if( is_root ):
+      for TM_f in ( TM_e for TM_e in files if TM_e.name.startswith(".") ):
+        st = os.lstat(TM_f.path)
+        entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name))
+      for TM_d in dirs:
+        st = os.lstat(TM_d.path)
+        entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_d.name + "/"))
+        gather(TM_d.path ,depth + 1 ,False)
+      for TM_f in ( TM_e for TM_e in files if not TM_e.name.startswith(".") ):
+        st = os.lstat(TM_f.path)
+        entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name))
     else:
-      for d in dirs:
-        st = os.lstat(d.path)
-        entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,d.name + "/"))
-        gather(d.path ,depth + 1 ,False)
-      for f in files:
-        st = os.lstat(f.path)
-        entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,f.name))
+      for TM_d in dirs:
+        st = os.lstat(TM_d.path)
+        entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_d.name + "/"))
+        gather(TM_d.path ,depth + 1 ,False)
+      for TM_f in files:
+        st = os.lstat(TM_f.path)
+        entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name))
 
   gather(root_dp ,1 ,True)
 
   ogw = 0
-  for _isdir ,_depth ,_perms ,ownergrp ,_name in entries:
-    if( len(ownergrp) > ogw ):
-      ogw = len(ownergrp)
+  for TM_isdir ,TM_depth ,TM_perms ,TM_ownergrp ,TM_name in entries:
+    if( len(TM_ownergrp) > ogw ):
+      ogw = len(TM_ownergrp)
 
   print("consumer/made/")
-  for isdir ,depth ,perms ,ownergrp ,name in entries:
-    indent = "  " * depth
-    print(f"{perms}  {ownergrp:<{ogw}}  {indent}{name}")
+  for TM_isdir ,TM_depth ,TM_perms ,TM_ownergrp ,TM_name in entries:
+    indent = "  " * TM_depth
+    print(f"{TM_perms}  {TM_ownergrp:<{ogw}}  {indent}{TM_name}")
 
 def copy_one(src_abs ,dst_abs ,mode ,dry=False):
   src_show = _display_src(src_abs)
@@ -152,7 +152,7 @@ def copy_one(src_abs ,dst_abs ,mode ,dry=False):
   parent = os.path.dirname(dst_abs)
   os.makedirs(parent ,exist_ok=True)
 
-  if(dry):
+  if( dry ):
     if( os.path.exists(dst_abs) ):
       print(f"(dry) unlink '{dst_show}'")
     print(f"(dry) install -m {oct(mode)[2:]} -D '{src_show}' '{dst_show}'")
@@ -186,18 +186,17 @@ def cmd_write(dry=False):
     exit_with_status(f"cannot find developer scratchpad made at '{_display_src(src_root)}'")
 
   wrote = False
-  for root ,dirs ,files in os.walk(src_root):
-    dirs.sort()
-    files.sort()
-    for fn in files:
-      src_abs = os.path.join(root ,fn)
+  for TM_root ,TM_dirs ,TM_files in os.walk(src_root):
+    TM_dirs.sort()
+    TM_files.sort()
+    for TM_fn in TM_files:
+      src_abs = os.path.join(TM_root ,TM_fn)
       rel = os.path.relpath(src_abs ,src_root)
       dst_abs = os.path.join(cpath() ,rel)
 
       if( os.path.exists(dst_abs) ):
-        src_mtime = os.stat(src_abs).st_mtime
-        dst_mtime = os.stat(dst_abs).st_mtime
-        if(dst_mtime >= src_mtime):
+        # Use content comparison instead of timestamps
+        if( filecmp.cmp(src_abs ,dst_abs ,shallow=False) ):
           continue
 
       st = os.stat(src_abs)
@@ -223,39 +222,59 @@ def cmd_diff():
     ,"made"
   )
 
-  if( not os.path.isdir(dst_root) ):
-    print(f"Consumer made directory {_display_dst(dst_root)} does not exist.")
+  src_files = set()
+  if( os.path.isdir(src_root) ):
+    for TM_root ,TM_dirs ,TM_files in os.walk(src_root):
+      for TM_fn in TM_files:
+        src_files.add(os.path.relpath(os.path.join(TM_root ,TM_fn) ,src_root))
+
+  dst_files = set()
+  if( os.path.isdir(dst_root) ):
+    for TM_root ,TM_dirs ,TM_files in os.walk(dst_root):
+      for TM_fn in TM_files:
+        dst_files.add(os.path.relpath(os.path.join(TM_root ,TM_fn) ,dst_root))
+
+  if( not src_files and not dst_files ):
+    print("No differences found. Both directories are empty or missing.")
     return
 
+  all_files = sorted(list(src_files | dst_files))
   found_diff = False
-  for root ,dirs ,files in os.walk(dst_root):
-    dirs.sort()
-    files.sort()
-    for fn in files:
-      dst_abs = os.path.join(root ,fn)
-      rel = os.path.relpath(dst_abs ,dst_root)
-      src_abs = os.path.join(src_root ,rel)
-
-      if( not os.path.exists(src_abs) ):
-        print(f"Orphaned in consumer made: {rel}")
-        found_diff = True
-      else:
-        dst_mtime = os.stat(dst_abs).st_mtime
+
+  for TM_rel in all_files:
+    if( TM_rel not in dst_files ):
+      print(f"Pending promotion (missing in consumer made): {TM_rel}")
+      found_diff = True
+    elif( TM_rel not in src_files ):
+      print(f"Orphaned in consumer made (missing in scratchpad): {TM_rel}")
+      found_diff = True
+    else:
+      src_abs = os.path.join(src_root ,TM_rel)
+      dst_abs = os.path.join(dst_root ,TM_rel)
+
+      # Compare contents first
+      if( not filecmp.cmp(src_abs ,dst_abs ,shallow=False) ):
         src_mtime = os.stat(src_abs).st_mtime
-        if(dst_mtime > src_mtime):
-          print(f"Newer in consumer made: {rel}")
-          found_diff = True
+        dst_mtime = os.stat(dst_abs).st_mtime
+
+        # If they differ, check timestamps to infer direction
+        if( src_mtime > dst_mtime ):
+          print(f"Pending update (contents differ, newer in scratchpad): {TM_rel}")
+        else:
+          print(f"Contents differ (locally modified in consumer made): {TM_rel}")
+        found_diff = True
 
   if( not found_diff ):
     print("No differences found. Consumer made matches developer scratchpad made.")
 
+
 def cmd_clean():
   assert_setup()
   consumer_root_dir = cpath()
   if( not os.path.isdir(consumer_root_dir) ):
     return
-  for name in os.listdir(consumer_root_dir):
-    p = os.path.join(consumer_root_dir ,name)
+  for TM_name in os.listdir(consumer_root_dir):
+    p = os.path.join(consumer_root_dir ,TM_name)
     if( os.path.isdir(p) and not os.path.islink(p) ):
       shutil.rmtree(p ,ignore_errors=True)
     else:
@@ -266,18 +285,21 @@ def CLI():
   if( len(sys.argv) < 2 ):
     print(HELP)
     return
-  cmd ,*args = sys.argv[1:]
-  if(cmd == "write"):
+  
+  cmd = sys.argv[1]
+  args = sys.argv[2:]
+
+  if( cmd == "write" ):
     cmd_write(dry=False)
-  elif(cmd == "clean"):
+  elif( cmd == "clean" ):
     cmd_clean()
-  elif(cmd == "ls"):
+  elif( cmd == "ls" ):
     list_tree(cpath())
-  elif(cmd == "diff"):
+  elif( cmd == "diff" ):
     cmd_diff()
-  elif(cmd == "help"):
+  elif( cmd == "help" ):
     print(HELP)
-  elif(cmd == "dry"):
+  elif( cmd == "dry" ):
     if( args and args[0] == "write" ):
       cmd_write(dry=True)
     else: