@@ -92,10 +92,40 @@ fn collect_spec_files(root: &Path, dir: &Path, acc: &mut Vec<PathBuf>) {
9292 }
9393}
9494
95+ /// What a declaration literally *says*, before lowering normalizes it away.
96+ ///
97+ /// Lowering is lossy by design: a typo'd `type = "srvice"` becomes `JobType::Service`, and an
98+ /// identity omitted from the content is filled in from the path. Both are invisible in the resolved
99+ /// [`AgentSpec`], so a linter that wants to fault them has to see the declared form. This is that
100+ /// view — deliberately narrow, so the permissive on-disk shape itself stays private and is free to
101+ /// gain fields without breaking readers.
102+ #[ derive( Debug , Clone , Default , PartialEq , Eq ) ]
103+ pub struct Declared {
104+ /// `identity` as written in the file. `None` when the file relies on [`path_defaults`].
105+ pub identity : Option < String > ,
106+ /// `type` as written, before it is normalized to `JobType::Service`. `None` when unset.
107+ pub job_type : Option < String > ,
108+ }
109+
110+ /// Read the declared (pre-lowering) values of every agent in a file — one per `agent` node for KDL,
111+ /// 0-or-1 for TOML/JSON, empty for a non-spec extension.
112+ ///
113+ /// One entry per parsed node, *including* nodes [`discover`] skips as non-specs, so this is not
114+ /// positionally paired with that file's [`Discovered::specs`].
115+ pub fn parse_declared ( path : & Path ) -> anyhow:: Result < Vec < Declared > > {
116+ Ok ( parse_raw_file ( path) ?
117+ . into_iter ( )
118+ . map ( |raw| Declared {
119+ identity : raw. identity ,
120+ job_type : raw. job_type ,
121+ } )
122+ . collect ( ) )
123+ }
124+
95125/// Parse a spec file into its raw (pre-resolution) shape — one per `agent` node for KDL, 0-or-1 for
96- /// TOML/JSON. Non-spec extensions yield an empty vec. Shared by discovery and `validate` (which needs
97- /// the *raw* `type` string before it is normalized away, to catch a typo'd `type = "srvice"` ).
98- pub ( crate ) fn parse_raw_file ( path : & Path ) -> anyhow:: Result < Vec < RawSpec > > {
126+ /// TOML/JSON. Non-spec extensions yield an empty vec. Shared by discovery and [`parse_declared`]
127+ /// (which exposes the *raw* `type` and `identity` before normalization, without leaking [`RawSpec`] ).
128+ fn parse_raw_file ( path : & Path ) -> anyhow:: Result < Vec < RawSpec > > {
99129 let ext = path. extension ( ) . and_then ( |e| e. to_str ( ) ) . unwrap_or ( "" ) ;
100130 let text = fs:: read_to_string ( path) ?;
101131 Ok ( match ext {
0 commit comments