cobolx 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/agent/client.rs +107 -8
- package/src/agent/db_agent.rs +71 -23
- package/src/agent/explain_agent.rs +53 -22
- package/src/agent/fs_agent.rs +211 -83
- package/src/agent/skills.rs +336 -0
- package/src/agent/types.rs +7 -0
- package/src/agent.rs +1 -0
- package/src/cobol/indexer.rs +375 -5
- package/src/cobol/model.rs +78 -0
- package/src/cobol/scanner.rs +2 -0
- package/src/cobol/source_parser.rs +341 -2
- package/src/lib.rs +1 -0
- package/src/main.rs +1 -0
- package/src/memory/memories.rs +208 -0
- package/src/memory/runs.rs +161 -0
- package/src/memory/store.rs +120 -0
- package/src/memory.rs +8 -2
- package/src/path_safety.rs +280 -0
- package/src/ui/draw.rs +1 -0
- package/src/ui/tui.rs +239 -0
- package/tests/indexer_tests.rs +261 -0
- package/tests/project_files_tests.rs +23 -51
- package/src/memory/files.rs +0 -155
package/src/ui/tui.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use crate::agent::client::AgentRouter;
|
|
2
|
+
use crate::memory::{RunJournal, TOKEN_SUMMARY_THRESHOLD};
|
|
2
3
|
use crate::ui::draw;
|
|
3
4
|
use chrono::Local;
|
|
4
5
|
use crossterm::{
|
|
@@ -7,6 +8,7 @@ use crossterm::{
|
|
|
7
8
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
|
8
9
|
};
|
|
9
10
|
use ratatui::{Terminal, backend::CrosstermBackend};
|
|
11
|
+
use serde_json::json;
|
|
10
12
|
use std::io;
|
|
11
13
|
use std::sync::Arc;
|
|
12
14
|
|
|
@@ -52,6 +54,10 @@ pub enum TaskUpdate {
|
|
|
52
54
|
Result<Option<crate::agent::client::Usage>, String>,
|
|
53
55
|
&'static str,
|
|
54
56
|
),
|
|
57
|
+
MemorySummarized {
|
|
58
|
+
ok: bool,
|
|
59
|
+
detail: String,
|
|
60
|
+
},
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
pub struct App {
|
|
@@ -80,6 +86,8 @@ pub struct App {
|
|
|
80
86
|
pub spinner_tick: usize,
|
|
81
87
|
pub console_scroll_offset: u16,
|
|
82
88
|
pub custom_prompt_override: Option<String>,
|
|
89
|
+
pub run_journal: Option<RunJournal>,
|
|
90
|
+
pub tokens_since_summary: u32,
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
impl App {
|
|
@@ -142,6 +150,38 @@ impl App {
|
|
|
142
150
|
spinner_tick: 0,
|
|
143
151
|
console_scroll_offset: 0,
|
|
144
152
|
custom_prompt_override: None,
|
|
153
|
+
run_journal: None,
|
|
154
|
+
tokens_since_summary: 0,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn start_run_journal(&mut self, sandbox: &std::path::Path) {
|
|
159
|
+
if let Ok(store) = crate::memory::MemoryStore::open_or_create(sandbox) {
|
|
160
|
+
let _ = store.codex_memories().ensure_layout();
|
|
161
|
+
if let Ok(journal) = RunJournal::start(store.runs_dir()) {
|
|
162
|
+
self.run_journal = Some(journal);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn log_run(&mut self, kind: &str, payload: serde_json::Value) {
|
|
168
|
+
if let Some(journal) = &mut self.run_journal {
|
|
169
|
+
journal.log(kind, payload);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn finish_run_journal(&mut self, status: &str) {
|
|
174
|
+
if let Some(journal) = &self.run_journal {
|
|
175
|
+
journal.finish(status);
|
|
176
|
+
if let Some(sandbox) = &self.sandbox_path {
|
|
177
|
+
if let Ok(store) = crate::memory::MemoryStore::open_or_create(sandbox) {
|
|
178
|
+
let mem = store.codex_memories();
|
|
179
|
+
if let Ok(log) = journal.read_log() {
|
|
180
|
+
let _ = mem.write_rollout_summary(journal.run_id(), &log);
|
|
181
|
+
let _ = mem.write_raw_memories(&log);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
145
185
|
}
|
|
146
186
|
}
|
|
147
187
|
|
|
@@ -155,6 +195,7 @@ impl App {
|
|
|
155
195
|
"/tokens".to_string(),
|
|
156
196
|
"/init".to_string(),
|
|
157
197
|
"/docs".to_string(),
|
|
198
|
+
"/skills".to_string(),
|
|
158
199
|
"/exit".to_string(),
|
|
159
200
|
];
|
|
160
201
|
if !self.input_text.starts_with('/') {
|
|
@@ -235,6 +276,14 @@ impl App {
|
|
|
235
276
|
timestamp: Local::now().format("%H:%M:%S").to_string(),
|
|
236
277
|
});
|
|
237
278
|
|
|
279
|
+
self.log_run(
|
|
280
|
+
"user_message",
|
|
281
|
+
json!({
|
|
282
|
+
"text": text,
|
|
283
|
+
"is_command": text.starts_with('/'),
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
|
|
238
287
|
let mut should_exit = false;
|
|
239
288
|
let mut is_command = false;
|
|
240
289
|
|
|
@@ -246,6 +295,7 @@ impl App {
|
|
|
246
295
|
let cmd = parts[0][1..].to_lowercase();
|
|
247
296
|
match cmd.as_str() {
|
|
248
297
|
"exit" | "quit" => {
|
|
298
|
+
self.log_run("session_exit", json!({ "reason": "user_command" }));
|
|
249
299
|
should_exit = true;
|
|
250
300
|
}
|
|
251
301
|
"clear" => {
|
|
@@ -276,6 +326,7 @@ impl App {
|
|
|
276
326
|
/tokens - Show model routing and token consumption statistics\n\
|
|
277
327
|
/init - Scan the sandbox directory for COBOL files\n\
|
|
278
328
|
/docs - Generate Markdown documentation for all COBOL files into docs/\n\
|
|
329
|
+
/skills - Check which agent skills (.cobolx/skills) are actually loaded\n\
|
|
279
330
|
/exit - Close the interactive console";
|
|
280
331
|
self.messages.push(Message {
|
|
281
332
|
sender: Sender::Cobolx,
|
|
@@ -302,6 +353,13 @@ impl App {
|
|
|
302
353
|
) {
|
|
303
354
|
Ok((report, db_path)) => {
|
|
304
355
|
self.discovered_files = report.files.clone();
|
|
356
|
+
self.log_run(
|
|
357
|
+
"index_completed",
|
|
358
|
+
json!({
|
|
359
|
+
"file_count": report.files.len(),
|
|
360
|
+
"db_path": db_path.to_string_lossy(),
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
305
363
|
if self.discovered_files.is_empty() {
|
|
306
364
|
self.messages.push(Message {
|
|
307
365
|
sender: Sender::Cobolx,
|
|
@@ -317,6 +375,7 @@ impl App {
|
|
|
317
375
|
}
|
|
318
376
|
}
|
|
319
377
|
Err(e) => {
|
|
378
|
+
self.log_run("index_failed", json!({ "error": e.to_string() }));
|
|
320
379
|
self.messages.push(Message {
|
|
321
380
|
sender: Sender::Cobolx,
|
|
322
381
|
text: format!("Error indexing sandbox: {}", e),
|
|
@@ -590,6 +649,73 @@ impl App {
|
|
|
590
649
|
});
|
|
591
650
|
}
|
|
592
651
|
}
|
|
652
|
+
"skills" => {
|
|
653
|
+
if let Some(ref sandbox_path) = self.sandbox_path {
|
|
654
|
+
let checks = crate::agent::skills::check_all_agent_skills(sandbox_path);
|
|
655
|
+
let mut report = String::from(
|
|
656
|
+
"Agent Skills Check (.cobolx/skills):\n\
|
|
657
|
+
-------------------------------------\n",
|
|
658
|
+
);
|
|
659
|
+
for check in &checks {
|
|
660
|
+
let dir = format!(".cobolx/skills/{}", check.agent.skill_dir());
|
|
661
|
+
if let Some(err) = &check.error {
|
|
662
|
+
report.push_str(&format!(
|
|
663
|
+
"[{}] {} → ERROR: {}\n",
|
|
664
|
+
check.agent.label(),
|
|
665
|
+
dir,
|
|
666
|
+
err
|
|
667
|
+
));
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if !check.dir_exists {
|
|
671
|
+
report.push_str(&format!(
|
|
672
|
+
"[{}] {} → not configured (no directory)\n",
|
|
673
|
+
check.agent.label(),
|
|
674
|
+
dir
|
|
675
|
+
));
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if check.files.is_empty() {
|
|
679
|
+
report.push_str(&format!(
|
|
680
|
+
"[{}] {} → directory exists, no .md skill files found\n",
|
|
681
|
+
check.agent.label(),
|
|
682
|
+
dir
|
|
683
|
+
));
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
let status = if check.is_active() {
|
|
687
|
+
"ACTIVE (loaded into system prompt)"
|
|
688
|
+
} else {
|
|
689
|
+
"INACTIVE (failed to load)"
|
|
690
|
+
};
|
|
691
|
+
report.push_str(&format!(
|
|
692
|
+
"[{}] {} → {}\n files: {} ({} bytes source, {} bytes injected{})\n",
|
|
693
|
+
check.agent.label(),
|
|
694
|
+
dir,
|
|
695
|
+
status,
|
|
696
|
+
check.files.len(),
|
|
697
|
+
check.source_bytes,
|
|
698
|
+
check.injected_bytes,
|
|
699
|
+
if check.truncated { ", TRUNCATED at 8KB cap" } else { "" }
|
|
700
|
+
));
|
|
701
|
+
for f in &check.files {
|
|
702
|
+
report.push_str(&format!(" - {}\n", f));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
self.messages.push(Message {
|
|
706
|
+
sender: Sender::Cobolx,
|
|
707
|
+
text: report.trim_end().to_string(),
|
|
708
|
+
timestamp: Local::now().format("%H:%M:%S").to_string(),
|
|
709
|
+
});
|
|
710
|
+
} else {
|
|
711
|
+
self.messages.push(Message {
|
|
712
|
+
sender: Sender::Cobolx,
|
|
713
|
+
text: "No sandbox directory set. Please select a sandbox first."
|
|
714
|
+
.to_string(),
|
|
715
|
+
timestamp: Local::now().format("%H:%M:%S").to_string(),
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
593
719
|
_ => {
|
|
594
720
|
self.messages.push(Message {
|
|
595
721
|
sender: Sender::Cobolx,
|
|
@@ -703,6 +829,66 @@ fn trigger_chat_task(app: &mut App, tx: &tokio::sync::mpsc::UnboundedSender<Task
|
|
|
703
829
|
false
|
|
704
830
|
}
|
|
705
831
|
|
|
832
|
+
fn spawn_memory_consolidation(
|
|
833
|
+
app: &App,
|
|
834
|
+
tx: &tokio::sync::mpsc::UnboundedSender<TaskUpdate>,
|
|
835
|
+
tokens_summarized: u32,
|
|
836
|
+
) {
|
|
837
|
+
let sandbox = app.sandbox_path.clone();
|
|
838
|
+
let router = Arc::clone(&app.router);
|
|
839
|
+
let run_log = app
|
|
840
|
+
.run_journal
|
|
841
|
+
.as_ref()
|
|
842
|
+
.and_then(|j| j.read_log().ok())
|
|
843
|
+
.unwrap_or_default();
|
|
844
|
+
let tx = tx.clone();
|
|
845
|
+
|
|
846
|
+
tokio::spawn(async move {
|
|
847
|
+
let _ = tx.send(TaskUpdate::Status(
|
|
848
|
+
"Consolidating memories (Codex Phase 2)...".to_string(),
|
|
849
|
+
));
|
|
850
|
+
let result: Result<(), String> = async {
|
|
851
|
+
let sandbox = sandbox.ok_or("No sandbox path for memory consolidation.")?;
|
|
852
|
+
let store =
|
|
853
|
+
crate::memory::MemoryStore::open_or_create(&sandbox).map_err(|e| e.to_string())?;
|
|
854
|
+
let mem = store.codex_memories();
|
|
855
|
+
let summary = mem.read_memory_summary().map_err(|e| e.to_string())?;
|
|
856
|
+
let handbook = mem.read_memory_handbook().map_err(|e| e.to_string())?;
|
|
857
|
+
let (new_summary, new_handbook) = router
|
|
858
|
+
.consolidate_codex_memories(&summary, &handbook, &run_log, tokens_summarized)
|
|
859
|
+
.await?;
|
|
860
|
+
mem.write_consolidated(&new_summary, &new_handbook)
|
|
861
|
+
.map_err(|e| e.to_string())?;
|
|
862
|
+
Ok(())
|
|
863
|
+
}
|
|
864
|
+
.await;
|
|
865
|
+
let _ = tx.send(TaskUpdate::Status("".to_string()));
|
|
866
|
+
match result {
|
|
867
|
+
Ok(()) => {
|
|
868
|
+
let _ = tx.send(TaskUpdate::MemorySummarized {
|
|
869
|
+
ok: true,
|
|
870
|
+
detail: format!("~{tokens_summarized} tokens → memory_summary.md + MEMORY.md"),
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
Err(err) => {
|
|
874
|
+
let _ = tx.send(TaskUpdate::MemorySummarized {
|
|
875
|
+
ok: false,
|
|
876
|
+
detail: err,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
fn route_label(route: &crate::agent::client::Route) -> &'static str {
|
|
884
|
+
match route {
|
|
885
|
+
crate::agent::client::Route::Light => "LIGHT",
|
|
886
|
+
crate::agent::client::Route::Heavy => "HEAVY",
|
|
887
|
+
crate::agent::client::Route::Database => "DATABASE",
|
|
888
|
+
crate::agent::client::Route::Filesystem => "FILESYSTEM",
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
706
892
|
pub fn run_tui() -> Result<(), io::Error> {
|
|
707
893
|
enable_raw_mode()?;
|
|
708
894
|
let mut stdout = io::stdout();
|
|
@@ -718,6 +904,13 @@ pub fn run_tui() -> Result<(), io::Error> {
|
|
|
718
904
|
while let Ok(update) = rx.try_recv() {
|
|
719
905
|
match update {
|
|
720
906
|
TaskUpdate::Routed(ref route, model_used) => {
|
|
907
|
+
app.log_run(
|
|
908
|
+
"route_selected",
|
|
909
|
+
json!({
|
|
910
|
+
"route": route_label(route),
|
|
911
|
+
"model": model_used,
|
|
912
|
+
}),
|
|
913
|
+
);
|
|
721
914
|
app.active_agent = Some(model_used.to_string());
|
|
722
915
|
if matches!(
|
|
723
916
|
route,
|
|
@@ -769,6 +962,9 @@ pub fn run_tui() -> Result<(), io::Error> {
|
|
|
769
962
|
}
|
|
770
963
|
}
|
|
771
964
|
TaskUpdate::Status(status) => {
|
|
965
|
+
if !status.is_empty() {
|
|
966
|
+
app.log_run("agent_status", json!({ "status": status }));
|
|
967
|
+
}
|
|
772
968
|
if status.is_empty() {
|
|
773
969
|
app.agent_status = None;
|
|
774
970
|
} else {
|
|
@@ -776,6 +972,25 @@ pub fn run_tui() -> Result<(), io::Error> {
|
|
|
776
972
|
}
|
|
777
973
|
}
|
|
778
974
|
TaskUpdate::Finished(res, model_used) => {
|
|
975
|
+
let payload = match &res {
|
|
976
|
+
Ok(Some(usage)) => json!({
|
|
977
|
+
"model": model_used,
|
|
978
|
+
"ok": true,
|
|
979
|
+
"prompt_tokens": usage.prompt_tokens,
|
|
980
|
+
"completion_tokens": usage.completion_tokens,
|
|
981
|
+
"total_tokens": usage.total_tokens,
|
|
982
|
+
}),
|
|
983
|
+
Ok(None) => json!({
|
|
984
|
+
"model": model_used,
|
|
985
|
+
"ok": true,
|
|
986
|
+
}),
|
|
987
|
+
Err(err) => json!({
|
|
988
|
+
"model": model_used,
|
|
989
|
+
"ok": false,
|
|
990
|
+
"error": err,
|
|
991
|
+
}),
|
|
992
|
+
};
|
|
993
|
+
app.log_run("agent_finished", payload);
|
|
779
994
|
app.active_agent = None;
|
|
780
995
|
app.agent_status = None;
|
|
781
996
|
if let Some(msg) = app.messages.iter_mut().last() {
|
|
@@ -802,6 +1017,13 @@ pub fn run_tui() -> Result<(), io::Error> {
|
|
|
802
1017
|
app.glm_prompt_tokens += usage.prompt_tokens;
|
|
803
1018
|
app.glm_completion_tokens += usage.completion_tokens;
|
|
804
1019
|
}
|
|
1020
|
+
|
|
1021
|
+
app.tokens_since_summary += usage.total_tokens;
|
|
1022
|
+
if app.tokens_since_summary >= TOKEN_SUMMARY_THRESHOLD {
|
|
1023
|
+
let tokens_batch = app.tokens_since_summary;
|
|
1024
|
+
app.tokens_since_summary = 0;
|
|
1025
|
+
spawn_memory_consolidation(&app, &tx, tokens_batch);
|
|
1026
|
+
}
|
|
805
1027
|
}
|
|
806
1028
|
Ok(None) => {
|
|
807
1029
|
app.last_model = Some(model_used.to_string());
|
|
@@ -820,6 +1042,16 @@ pub fn run_tui() -> Result<(), io::Error> {
|
|
|
820
1042
|
}
|
|
821
1043
|
}
|
|
822
1044
|
}
|
|
1045
|
+
TaskUpdate::MemorySummarized { ok, detail } => {
|
|
1046
|
+
app.log_run("memory_summarized", json!({ "ok": ok, "detail": detail }));
|
|
1047
|
+
if ok {
|
|
1048
|
+
app.messages.push(Message {
|
|
1049
|
+
sender: Sender::Cobolx,
|
|
1050
|
+
text: format!("Memory consolidated ({detail})."),
|
|
1051
|
+
timestamp: Local::now().format("%H:%M:%S").to_string(),
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
823
1055
|
}
|
|
824
1056
|
}
|
|
825
1057
|
|
|
@@ -928,7 +1160,12 @@ pub fn run_tui() -> Result<(), io::Error> {
|
|
|
928
1160
|
.and_then(|p| p.parent().map(|parent| parent.to_path_buf()))
|
|
929
1161
|
};
|
|
930
1162
|
if let Some(path) = resolved {
|
|
1163
|
+
app.start_run_journal(&path);
|
|
931
1164
|
app.sandbox_path = Some(path.clone());
|
|
1165
|
+
app.log_run(
|
|
1166
|
+
"sandbox_selected",
|
|
1167
|
+
json!({ "path": path.to_string_lossy() }),
|
|
1168
|
+
);
|
|
932
1169
|
app.view_mode = ViewMode::Chat;
|
|
933
1170
|
app.messages.push(Message {
|
|
934
1171
|
sender: Sender::Cobolx,
|
|
@@ -1048,6 +1285,8 @@ pub fn run_tui() -> Result<(), io::Error> {
|
|
|
1048
1285
|
}
|
|
1049
1286
|
}
|
|
1050
1287
|
|
|
1288
|
+
app.finish_run_journal("completed");
|
|
1289
|
+
|
|
1051
1290
|
disable_raw_mode()?;
|
|
1052
1291
|
execute!(
|
|
1053
1292
|
terminal.backend_mut(),
|
package/tests/indexer_tests.rs
CHANGED
|
@@ -190,3 +190,264 @@ fn init_indexer_persists_program_copybook_and_call_edges() {
|
|
|
190
190
|
assert_eq!(alias_layout, (8, 10));
|
|
191
191
|
assert_eq!(occurs_layout, (23, 60));
|
|
192
192
|
}
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn init_indexer_persists_richer_semantic_index_data() {
|
|
196
|
+
let dir = tempdir().unwrap();
|
|
197
|
+
let copy_dir = dir.path().join("copy");
|
|
198
|
+
fs::create_dir_all(©_dir).unwrap();
|
|
199
|
+
|
|
200
|
+
File::create(dir.path().join("MAIN.cbl"))
|
|
201
|
+
.unwrap()
|
|
202
|
+
.write_all(
|
|
203
|
+
br#"
|
|
204
|
+
IDENTIFICATION DIVISION.
|
|
205
|
+
PROGRAM-ID. MAIN.
|
|
206
|
+
DATA DIVISION.
|
|
207
|
+
WORKING-STORAGE SECTION.
|
|
208
|
+
01 WS-NEXT-PGM PIC X(8).
|
|
209
|
+
01 CUSTOMER-FILE PIC X(8).
|
|
210
|
+
01 CUSTOMER-RECORD PIC X(80).
|
|
211
|
+
COPY COMMONHDR.
|
|
212
|
+
COPY ERRCODES.
|
|
213
|
+
PROCEDURE DIVISION.
|
|
214
|
+
MAIN-SECTION SECTION.
|
|
215
|
+
MAIN-START.
|
|
216
|
+
OPEN INPUT CUSTOMER-FILE.
|
|
217
|
+
EXEC SQL
|
|
218
|
+
SELECT CUST_ID
|
|
219
|
+
INTO :WS-NEXT-PGM
|
|
220
|
+
FROM CUSTOMER_TABLE
|
|
221
|
+
WHERE STATUS = 'ER'
|
|
222
|
+
END-EXEC.
|
|
223
|
+
CALL "SUB001"
|
|
224
|
+
USING WS-NEXT-PGM.
|
|
225
|
+
CALL WS-NEXT-PGM.
|
|
226
|
+
PERFORM HANDLE-ERROR.
|
|
227
|
+
READ CUSTOMER-FILE.
|
|
228
|
+
HANDLE-ERROR.
|
|
229
|
+
EXEC CICS
|
|
230
|
+
LINK PROGRAM('ERRHNDL')
|
|
231
|
+
COMMAREA(WS-NEXT-PGM)
|
|
232
|
+
END-EXEC.
|
|
233
|
+
WRITE CUSTOMER-RECORD.
|
|
234
|
+
CLOSE CUSTOMER-FILE.
|
|
235
|
+
STOP RUN.
|
|
236
|
+
"#,
|
|
237
|
+
)
|
|
238
|
+
.unwrap();
|
|
239
|
+
|
|
240
|
+
File::create(dir.path().join("SUB001.cbl"))
|
|
241
|
+
.unwrap()
|
|
242
|
+
.write_all(
|
|
243
|
+
br#"
|
|
244
|
+
IDENTIFICATION DIVISION.
|
|
245
|
+
PROGRAM-ID. SUB001.
|
|
246
|
+
PROCEDURE DIVISION.
|
|
247
|
+
EXIT PROGRAM.
|
|
248
|
+
"#,
|
|
249
|
+
)
|
|
250
|
+
.unwrap();
|
|
251
|
+
|
|
252
|
+
File::create(copy_dir.join("COMMONHDR.cpy"))
|
|
253
|
+
.unwrap()
|
|
254
|
+
.write_all(
|
|
255
|
+
br#"
|
|
256
|
+
01 COMMON-HDR.
|
|
257
|
+
05 HDR-REQUEST-ID PIC X(16).
|
|
258
|
+
05 HDR-DATE PIC X(8).
|
|
259
|
+
"#,
|
|
260
|
+
)
|
|
261
|
+
.unwrap();
|
|
262
|
+
|
|
263
|
+
File::create(copy_dir.join("ERRCODES.cpy"))
|
|
264
|
+
.unwrap()
|
|
265
|
+
.write_all(
|
|
266
|
+
br#"
|
|
267
|
+
01 ERROR-INFO.
|
|
268
|
+
05 ERR-CODE PIC X(4).
|
|
269
|
+
05 ERR-MESSAGE PIC X(40).
|
|
270
|
+
"#,
|
|
271
|
+
)
|
|
272
|
+
.unwrap();
|
|
273
|
+
|
|
274
|
+
let mut store = rdo::memory::MemoryStore::open_or_create(dir.path()).unwrap();
|
|
275
|
+
rdo::cobol::indexer::index_sandbox(dir.path(), &mut store).unwrap();
|
|
276
|
+
|
|
277
|
+
let conn = store.connection();
|
|
278
|
+
|
|
279
|
+
let main_program_features: (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) = conn
|
|
280
|
+
.query_row(
|
|
281
|
+
"SELECT incoming_call_count, outgoing_call_count, static_call_count, dynamic_call_count, \
|
|
282
|
+
copybook_use_count, referenced_by_file_count, is_entrypoint, paragraph_count, \
|
|
283
|
+
external_op_count, literal_count \
|
|
284
|
+
FROM program_features pf \
|
|
285
|
+
JOIN programs p ON p.id = pf.program_id \
|
|
286
|
+
WHERE p.name = 'MAIN'",
|
|
287
|
+
[],
|
|
288
|
+
|row| {
|
|
289
|
+
Ok((
|
|
290
|
+
row.get(0)?,
|
|
291
|
+
row.get(1)?,
|
|
292
|
+
row.get(2)?,
|
|
293
|
+
row.get(3)?,
|
|
294
|
+
row.get(4)?,
|
|
295
|
+
row.get(5)?,
|
|
296
|
+
row.get(6)?,
|
|
297
|
+
row.get(7)?,
|
|
298
|
+
row.get(8)?,
|
|
299
|
+
row.get(9)?,
|
|
300
|
+
))
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
.unwrap();
|
|
304
|
+
|
|
305
|
+
let sub_program_features: (i64, i64, i64) = conn
|
|
306
|
+
.query_row(
|
|
307
|
+
"SELECT incoming_call_count, referenced_by_file_count, is_entrypoint \
|
|
308
|
+
FROM program_features pf \
|
|
309
|
+
JOIN programs p ON p.id = pf.program_id \
|
|
310
|
+
WHERE p.name = 'SUB001'",
|
|
311
|
+
[],
|
|
312
|
+
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
|
313
|
+
)
|
|
314
|
+
.unwrap();
|
|
315
|
+
|
|
316
|
+
let code_blocks: Vec<(String, String, i64)> = {
|
|
317
|
+
let mut stmt = conn
|
|
318
|
+
.prepare(
|
|
319
|
+
"SELECT cb.name, cb.kind, cb.statement_count \
|
|
320
|
+
FROM code_blocks cb \
|
|
321
|
+
JOIN programs p ON p.id = cb.program_id \
|
|
322
|
+
WHERE p.name = 'MAIN' \
|
|
323
|
+
ORDER BY sequence_no",
|
|
324
|
+
)
|
|
325
|
+
.unwrap();
|
|
326
|
+
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
|
327
|
+
.unwrap()
|
|
328
|
+
.collect::<Result<Vec<_>, _>>()
|
|
329
|
+
.unwrap()
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
let external_ops: Vec<(String, String, Option<String>)> = {
|
|
333
|
+
let mut stmt = conn
|
|
334
|
+
.prepare(
|
|
335
|
+
"SELECT kind, verb, target \
|
|
336
|
+
FROM external_ops eo \
|
|
337
|
+
JOIN programs p ON p.id = eo.program_id \
|
|
338
|
+
WHERE p.name = 'MAIN' \
|
|
339
|
+
ORDER BY eo.id",
|
|
340
|
+
)
|
|
341
|
+
.unwrap();
|
|
342
|
+
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
|
343
|
+
.unwrap()
|
|
344
|
+
.collect::<Result<Vec<_>, _>>()
|
|
345
|
+
.unwrap()
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
let identifiers: Vec<(String, String, i64)> = {
|
|
349
|
+
let mut stmt = conn
|
|
350
|
+
.prepare(
|
|
351
|
+
"SELECT kind, value, occurrences \
|
|
352
|
+
FROM identifiers i \
|
|
353
|
+
JOIN programs p ON p.id = i.program_id \
|
|
354
|
+
WHERE p.name = 'MAIN' \
|
|
355
|
+
ORDER BY kind, value",
|
|
356
|
+
)
|
|
357
|
+
.unwrap();
|
|
358
|
+
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
|
359
|
+
.unwrap()
|
|
360
|
+
.collect::<Result<Vec<_>, _>>()
|
|
361
|
+
.unwrap()
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
let literals: Vec<(String, String, i64)> = {
|
|
365
|
+
let mut stmt = conn
|
|
366
|
+
.prepare(
|
|
367
|
+
"SELECT kind, value, occurrences \
|
|
368
|
+
FROM literals l \
|
|
369
|
+
JOIN programs p ON p.id = l.program_id \
|
|
370
|
+
WHERE p.name = 'MAIN' \
|
|
371
|
+
ORDER BY kind, value",
|
|
372
|
+
)
|
|
373
|
+
.unwrap();
|
|
374
|
+
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
|
375
|
+
.unwrap()
|
|
376
|
+
.collect::<Result<Vec<_>, _>>()
|
|
377
|
+
.unwrap()
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
let copybook_features: Vec<(String, i64, i64, i64, i64, i64)> = {
|
|
381
|
+
let mut stmt = conn
|
|
382
|
+
.prepare(
|
|
383
|
+
"SELECT cf.copybook_name, cf.used_by_program_count, cf.used_by_file_count, \
|
|
384
|
+
cf.data_item_count, cf.contains_header_fields, cf.contains_error_fields \
|
|
385
|
+
FROM copybook_features cf \
|
|
386
|
+
ORDER BY cf.copybook_name",
|
|
387
|
+
)
|
|
388
|
+
.unwrap();
|
|
389
|
+
stmt.query_map([], |row| {
|
|
390
|
+
Ok((
|
|
391
|
+
row.get(0)?,
|
|
392
|
+
row.get(1)?,
|
|
393
|
+
row.get(2)?,
|
|
394
|
+
row.get(3)?,
|
|
395
|
+
row.get(4)?,
|
|
396
|
+
row.get(5)?,
|
|
397
|
+
))
|
|
398
|
+
})
|
|
399
|
+
.unwrap()
|
|
400
|
+
.collect::<Result<Vec<_>, _>>()
|
|
401
|
+
.unwrap()
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
assert_eq!(main_program_features, (0, 2, 1, 1, 2, 0, 1, 2, 8, 3));
|
|
405
|
+
assert_eq!(sub_program_features, (1, 1, 0));
|
|
406
|
+
assert_eq!(
|
|
407
|
+
code_blocks,
|
|
408
|
+
vec![
|
|
409
|
+
("MAIN-SECTION".to_string(), "section".to_string(), 0),
|
|
410
|
+
("MAIN-START".to_string(), "paragraph".to_string(), 6),
|
|
411
|
+
("HANDLE-ERROR".to_string(), "paragraph".to_string(), 4),
|
|
412
|
+
]
|
|
413
|
+
);
|
|
414
|
+
assert!(external_ops.contains(&(
|
|
415
|
+
"file_io".to_string(),
|
|
416
|
+
"OPEN".to_string(),
|
|
417
|
+
Some("CUSTOMER-FILE".to_string())
|
|
418
|
+
)));
|
|
419
|
+
assert!(external_ops.contains(&(
|
|
420
|
+
"exec_sql".to_string(),
|
|
421
|
+
"SELECT".to_string(),
|
|
422
|
+
Some("CUSTOMER_TABLE".to_string())
|
|
423
|
+
)));
|
|
424
|
+
assert!(external_ops.contains(&(
|
|
425
|
+
"call_literal".to_string(),
|
|
426
|
+
"CALL".to_string(),
|
|
427
|
+
Some("SUB001".to_string())
|
|
428
|
+
)));
|
|
429
|
+
assert!(external_ops.contains(&(
|
|
430
|
+
"call_identifier".to_string(),
|
|
431
|
+
"CALL".to_string(),
|
|
432
|
+
Some("WS-NEXT-PGM".to_string())
|
|
433
|
+
)));
|
|
434
|
+
assert!(external_ops.contains(&(
|
|
435
|
+
"exec_cics".to_string(),
|
|
436
|
+
"LINK".to_string(),
|
|
437
|
+
Some("ERRHNDL".to_string())
|
|
438
|
+
)));
|
|
439
|
+
assert!(identifiers.contains(&("sql_table".to_string(), "CUSTOMER_TABLE".to_string(), 1)));
|
|
440
|
+
assert!(identifiers.contains(&("file_name".to_string(), "CUSTOMER-FILE".to_string(), 3)));
|
|
441
|
+
assert!(identifiers.contains(&("paragraph_name".to_string(), "HANDLE-ERROR".to_string(), 1)));
|
|
442
|
+
assert!(identifiers.contains(&("data_name".to_string(), "ERR-CODE".to_string(), 1)));
|
|
443
|
+
assert!(literals.contains(&("call_target".to_string(), "SUB001".to_string(), 1)));
|
|
444
|
+
assert!(literals.contains(&("exec_cics_program".to_string(), "ERRHNDL".to_string(), 1)));
|
|
445
|
+
assert!(literals.contains(&("string_literal".to_string(), "ER".to_string(), 1)));
|
|
446
|
+
assert_eq!(
|
|
447
|
+
copybook_features,
|
|
448
|
+
vec![
|
|
449
|
+
("COMMONHDR".to_string(), 1, 1, 3, 1, 0),
|
|
450
|
+
("ERRCODES".to_string(), 1, 1, 3, 0, 1),
|
|
451
|
+
]
|
|
452
|
+
);
|
|
453
|
+
}
|