Capture Fennel source line for each CI job
mlua stack inspection in the `job` UserData callback grabs the
calling line; Fennel's `correlate` opt keeps Lua and Fennel line
numbers aligned. Sets up source-span diagnostics for upcoming
per-job validation errors.

Assisted-by: Claude Opus 4.7 via Claude Code
change xukwlxzuktpulsuquwzlwvmqrnnxsypm
commit b3f62601f6ea7eb29320a7490b2254c070b2abbf
author Alpha Chen <alpha@kejadlen.dev>
date
parent tosuurqv
diff --git a/src/ci/pipeline.rs b/src/ci/pipeline.rs
index c656124..4a03120 100644
--- a/src/ci/pipeline.rs
+++ b/src/ci/pipeline.rs
@@ -12,6 +12,9 @@ use crate::fennel::Fennel;
 pub struct Job {
     pub id: String,
     pub inputs: Vec<String>,
+    /// 1-indexed line in the source where `(ci:job …)` was called.
+    /// `0` means the line could not be determined.
+    pub line: u32,
     /// The job's run function from the Lua VM.
     /// Stored for future execution — not yet called.
     #[expect(dead_code)]
@@ -50,8 +53,18 @@ impl UserData for CiModule {
     fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
         methods.add_method(
             "job",
-            |_, this, (id, inputs, run_fn): (String, Vec<String>, mlua::Function)| {
-                this.jobs.borrow_mut().push(Job { id, inputs, run_fn });
+            |lua, this, (id, inputs, run_fn): (String, Vec<String>, mlua::Function)| {
+                let line = lua
+                    .inspect_stack(1, |d| d.current_line())
+                    .flatten()
+                    .map(|l| l as u32)
+                    .unwrap_or(0);
+                this.jobs.borrow_mut().push(Job {
+                    id,
+                    inputs,
+                    line,
+                    run_fn,
+                });
                 Ok(())
             },
         );
@@ -239,6 +252,20 @@ mod tests {
         assert_eq!(jobs[1].inputs, vec!["build"]);
     }
 
+    #[test]
+    fn load_captures_source_line() {
+        let f = fennel();
+        let source = "(local ci (require :quire.ci))
+(ci:job :first [:quire/push] (fn [_] nil))
+(ci:job :second [:quire/push] (fn [_] nil))
+
+
+(ci:job :sixth [:quire/push] (fn [_] nil))";
+        let pipeline = load(&f, source, "ci.fnl", "ci.fnl").expect("load should succeed");
+        let lines: Vec<u32> = pipeline.jobs().iter().map(|j| j.line).collect();
+        assert_eq!(lines, vec![2, 3, 6]);
+    }
+
     #[test]
     fn load_errors_on_bad_fennel() {
         let f = fennel();
diff --git a/src/fennel.rs b/src/fennel.rs
index 162a09e..3b9e20d 100644
--- a/src/fennel.rs
+++ b/src/fennel.rs
@@ -96,6 +96,9 @@ impl Fennel {
         let opts = self.lua.create_table()?;
 
         opts.set("filename", filename)?;
+        // Align Lua line numbers with Fennel source lines so debug
+        // info points back at the user's `.fnl`.
+        opts.set("correlate", true)?;
 
         let result = eval
             .call::<mlua::Value>((source, opts))