Skip to content

Commit 8e32a23

Browse files
committed
[gobby-cli-#764] feat: densify codewiki tables
1 parent cda4723 commit 8e32a23

11 files changed

Lines changed: 228 additions & 120 deletions

File tree

crates/gcode/src/commands/codewiki/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const OWNERSHIP_META_PATH: &str = "_meta/ownership.json";
1111
const MAX_MERMAID_HOPS: usize = 2;
1212
const MAX_MERMAID_EDGES: usize = 20;
1313
const MAX_EDGE_LIMIT: usize = 100_000;
14-
const CODEWIKI_RENDER_VERSION: u32 = 2;
14+
const CODEWIKI_RENDER_VERSION: u32 = 3;
1515

1616
/// Default daemon feature profile for aggregate (module/repo/architecture)
1717
/// prose, which synthesizes 10k+-token grounded prompts; file and symbol
@@ -66,7 +66,8 @@ pub(crate) use progress::CodewikiProgress;
6666
pub(crate) use paths::{
6767
component_label, direct_child_modules, file_doc_path, file_wikilink, in_scope, inline_code,
6868
is_core_file, module_ancestors, module_depth, module_doc_path, module_for_file,
69-
module_is_ancestor, module_wikilink, parent_module, plural,
69+
module_is_ancestor, module_wikilink, parent_module, plural, write_markdown_table_header,
70+
write_markdown_table_row,
7071
};
7172
// Rendered markdown and graph diagrams.
7273
pub(crate) use render::{

crates/gcode/src/commands/codewiki/paths.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,33 @@ pub(crate) fn inline_code(value: &str) -> String {
1313
}
1414
}
1515

16+
pub(crate) fn write_markdown_table_header(doc: &mut String, headers: &[&str]) {
17+
write_markdown_table_row(doc, headers.iter().copied());
18+
write_markdown_table_row(doc, (0..headers.len()).map(|_| "---"));
19+
}
20+
21+
pub(crate) fn write_markdown_table_row<I, S>(doc: &mut String, cells: I)
22+
where
23+
I: IntoIterator<Item = S>,
24+
S: AsRef<str>,
25+
{
26+
doc.push('|');
27+
for cell in cells {
28+
doc.push(' ');
29+
doc.push_str(&markdown_table_cell(cell.as_ref()));
30+
doc.push_str(" |");
31+
}
32+
doc.push('\n');
33+
}
34+
35+
fn markdown_table_cell(value: &str) -> String {
36+
value
37+
.split_whitespace()
38+
.collect::<Vec<_>>()
39+
.join(" ")
40+
.replace('|', "\\|")
41+
}
42+
1643
pub(crate) fn max_backtick_run(value: &str) -> usize {
1744
let mut max_run = 0usize;
1845
let mut current_run = 0usize;

crates/gcode/src/commands/codewiki/prompts.rs

Lines changed: 104 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ use std::fmt::Write as _;
22

33
use crate::models::Symbol;
44

5+
use super::{write_markdown_table_header, write_markdown_table_row};
6+
57
pub const SYMBOL_SYSTEM: &str = "You write concise API reference notes. Return one sentence describing the symbol's purpose. Do not include markdown fences.";
68
pub const FILE_SYSTEM: &str = "You write concise file-level code documentation. Return a short purpose summary grounded only in the supplied symbol summaries and source excerpt: what the file does and how its pieces work together. Do not include markdown fences.";
79
pub const CONTENT_FILE_SYSTEM: &str = "You write concise documentation for non-code repository files. Return a short purpose summary describing what the file contains and what it is for, grounded only in the supplied leading content. Do not include markdown fences.";
8-
pub const MODULE_SYSTEM: &str = "You write module documentation briefs. Using only the supplied file summaries, child module summaries, and source excerpts, write two to three short paragraphs covering the module's responsibilities, its key flows, and how its files and submodules collaborate. Plain paragraphs only - no headings, no lists, no markdown fences. Cite supporting file:line spans that appear in the supplied input.";
9-
pub const REPO_SYSTEM: &str = "You write repository overview briefs. Using only the supplied module summaries, root-file summaries, and source excerpts, write two to three short paragraphs covering what the system is, how the major pieces fit together, and where a reader should start. Plain paragraphs only - no headings, no lists, no markdown fences. Cite supporting file:line spans that appear in the supplied input.";
10-
pub const ARCHITECTURE_SYSTEM: &str = "You write concise architecture documentation. Using only the supplied summaries and source excerpts, return one to two sentences naming the subsystem's responsibility and how it collaborates with the rest of the system. Do not include markdown fences.";
10+
pub const MODULE_SYSTEM: &str = "You write module documentation briefs. Using only the supplied file summaries, child module summaries, component table, and source excerpts, write one to two short paragraphs covering the module's responsibilities, key flows, and collaboration points. Add compact Markdown tables for enumerable facts such as CLI commands or flags, configuration keys, environment variables, and public API symbols. No markdown fences. Cite supporting file:line spans that appear in the supplied input.";
11+
pub const REPO_SYSTEM: &str = "You write repository overview briefs. Using only the supplied module summaries, root-file summaries, and source excerpts, write one to two short paragraphs covering what the system is, how the major pieces fit together, and where a reader should start. Add compact Markdown tables for enumerable facts such as CLI commands or flags, configuration keys, environment variables, and public API symbols. No markdown fences. Cite supporting file:line spans that appear in the supplied input.";
12+
pub const ARCHITECTURE_SYSTEM: &str = "You write concise architecture documentation. Using only the supplied summaries, component table, and source excerpts, return a short responsibility summary plus compact Markdown tables for enumerable facts such as public API symbols, CLI commands or flags, configuration keys, and environment variables. No markdown fences.";
1113
pub const ARCHITECTURE_NARRATIVE_SYSTEM: &str = "You write architecture overviews. Using only the supplied subsystem responsibilities and dependency edges, write two to three short paragraphs describing the system in layers: which subsystems sit at the foundation, which build on them, and how the layers interact. Plain paragraphs only - no headings, no lists, no markdown fences.";
1214
pub const CURATED_NAVIGATION_SYSTEM: &str = "You design a curated navigation layer for grounded code documentation. Return strict JSON only. Name user-facing concept modules, organize them into a hierarchy, and create short narrative tour pages. Use only supplied module and file identifiers, and link into reference pages instead of duplicating source detail.";
1315

@@ -41,17 +43,28 @@ pub fn file_prompt(file: &str, symbols: &[SymbolSummary], sources: &[SourceExcer
4143
if symbols.is_empty() {
4244
prompt.push_str("- No indexed symbols.\n");
4345
} else {
46+
write_markdown_table_header(
47+
&mut prompt,
48+
&[
49+
"Symbol",
50+
"Kind",
51+
"Component",
52+
"Component ID",
53+
"Lines",
54+
"Purpose",
55+
],
56+
);
4457
for symbol in symbols {
45-
let _ = writeln!(
46-
prompt,
47-
"- {} [{}] component {} ({}) lines {}-{}: {}",
48-
symbol.name,
49-
symbol.kind,
50-
symbol.component_label,
51-
symbol.component_id,
52-
symbol.line_start,
53-
symbol.line_end,
54-
symbol.purpose
58+
write_markdown_table_row(
59+
&mut prompt,
60+
[
61+
symbol.name.clone(),
62+
symbol.kind.clone(),
63+
symbol.component_label.clone(),
64+
symbol.component_id.clone(),
65+
format!("{}-{}", symbol.line_start, symbol.line_end),
66+
symbol.purpose.clone(),
67+
],
5568
);
5669
}
5770
}
@@ -93,37 +106,48 @@ pub fn repo_prompt(
93106
sources: &[SourceExcerpt],
94107
) -> String {
95108
let mut prompt =
96-
"Write a repository overview brief from module summaries, root-file summaries, and source excerpts.\n\nModules:\n"
109+
"Write a repository overview brief from module summaries, root-file summaries, and source excerpts.\n\n"
97110
.to_string();
111+
append_table_guidance(&mut prompt);
112+
prompt.push_str("Modules:\n");
98113
if modules.is_empty() {
99114
prompt.push_str("- No modules.\n");
100115
} else {
101-
for module in modules {
102-
let _ = writeln!(
103-
prompt,
104-
"- {}: {}",
105-
module.name,
106-
summary_excerpt(&module.summary)
107-
);
108-
}
116+
append_child_summary_table(&mut prompt, &["Module", "Summary"], modules);
109117
}
110118
prompt.push_str("\nRoot files:\n");
111119
if files.is_empty() {
112120
prompt.push_str("- No root files.\n");
113121
} else {
114-
for file in files {
115-
let _ = writeln!(
116-
prompt,
117-
"- {}: {}",
118-
file.name,
119-
summary_excerpt(&file.summary)
120-
);
121-
}
122+
append_child_summary_table(&mut prompt, &["File", "Summary"], files);
122123
}
123124
append_source_excerpt_section(&mut prompt, sources);
124125
prompt
125126
}
126127

128+
fn append_child_summary_table(prompt: &mut String, headers: &[&str], children: &[ChildSummary]) {
129+
write_markdown_table_header(prompt, headers);
130+
for child in children {
131+
write_markdown_table_row(
132+
prompt,
133+
[child.name.clone(), summary_excerpt(&child.summary)],
134+
);
135+
}
136+
}
137+
138+
fn append_component_table(prompt: &mut String, components: &[String]) {
139+
write_markdown_table_header(prompt, &["Component"]);
140+
for component in components {
141+
write_markdown_table_row(prompt, [component.clone()]);
142+
}
143+
}
144+
145+
fn append_table_guidance(prompt: &mut String) {
146+
prompt.push_str("Table guidance:\n");
147+
prompt.push_str(ENUMERABLE_FACTS_GUIDANCE);
148+
prompt.push_str("\n\n");
149+
}
150+
127151
pub fn architecture_prompt(
128152
subsystem: &str,
129153
files: &[ChildSummary],
@@ -184,7 +208,9 @@ fn build_entity_prompt(
184208
components: &[String],
185209
sources: &[SourceExcerpt],
186210
) -> String {
187-
let mut prompt = format!("{header}\n\n{entity_label}: {entity}\n\nFiles:\n");
211+
let mut prompt = format!("{header}\n\n{entity_label}: {entity}\n\n");
212+
append_table_guidance(&mut prompt);
213+
prompt.push_str("Files:\n");
188214
append_child_summary_sections(&mut prompt, files, modules, components);
189215
append_source_excerpt_section(&mut prompt, sources);
190216
prompt
@@ -199,35 +225,19 @@ fn append_child_summary_sections(
199225
if files.is_empty() {
200226
prompt.push_str("- No direct files.\n");
201227
} else {
202-
for file in files {
203-
let _ = writeln!(
204-
prompt,
205-
"- {}: {}",
206-
file.name,
207-
summary_excerpt(&file.summary)
208-
);
209-
}
228+
append_child_summary_table(prompt, &["File", "Summary"], files);
210229
}
211230
prompt.push_str("\nChild modules:\n");
212231
if modules.is_empty() {
213232
prompt.push_str("- No child modules.\n");
214233
} else {
215-
for module in modules {
216-
let _ = writeln!(
217-
prompt,
218-
"- {}: {}",
219-
module.name,
220-
summary_excerpt(&module.summary)
221-
);
222-
}
234+
append_child_summary_table(prompt, &["Module", "Summary"], modules);
223235
}
224236
prompt.push_str("\nStable component IDs:\n");
225237
if components.is_empty() {
226238
prompt.push_str("- No indexed components.\n");
227239
} else {
228-
for component in components {
229-
let _ = writeln!(prompt, "- {component}");
230-
}
240+
append_component_table(prompt, components);
231241
}
232242
}
233243

@@ -260,6 +270,7 @@ const CHILD_SUMMARY_EXCERPT_MAX_CHARS: usize = 2_000;
260270
/// even though they now carry real source content.
261271
pub(crate) const SOURCE_EXCERPT_MAX_CHARS: usize = 2_400;
262272
pub(crate) const MAX_PROMPT_SOURCE_EXCERPTS: usize = 4;
273+
const ENUMERABLE_FACTS_GUIDANCE: &str = "When the supplied input exposes enumerable facts (CLI commands/flags, configuration keys, environment variables, or public API symbols), prefer compact Markdown tables beside the narrative instead of burying those facts in prose.";
263274

264275
/// First paragraph of a child summary, flattened to one line and hard-capped
265276
/// at [`CHILD_SUMMARY_EXCERPT_MAX_CHARS`], so each prompt list entry stays one
@@ -355,14 +366,14 @@ mod tests {
355366
architecture_prompt("src", &children, &children, &[], &[]),
356367
repo_prompt(&children, &children, &[]),
357368
] {
358-
for line in prompt.lines().filter(|line| line.starts_with("- src/")) {
369+
for line in prompt.lines().filter(|line| line.starts_with("| src/")) {
359370
assert!(
360371
line.chars().count()
361-
<= CHILD_SUMMARY_EXCERPT_MAX_CHARS + "- src/module_0: …".chars().count(),
372+
<= CHILD_SUMMARY_EXCERPT_MAX_CHARS + "| src/module_0 | |".chars().count(),
362373
"child summary line stays bounded: {} chars",
363374
line.chars().count()
364375
);
365-
assert!(line.ends_with('…'), "oversized excerpt is marked truncated");
376+
assert!(line.contains('…'), "oversized excerpt is marked truncated");
366377
}
367378
}
368379
}
@@ -374,7 +385,7 @@ mod tests {
374385
summary: "Concise healthy summary.".to_string(),
375386
};
376387
let prompt = module_prompt("src", &[child], &[], &[], &[]);
377-
assert!(prompt.contains("- src/lib.rs: Concise healthy summary.\n"));
388+
assert!(prompt.contains("| src/lib.rs | Concise healthy summary. |\n"));
378389
}
379390

380391
#[test]
@@ -384,7 +395,45 @@ mod tests {
384395
summary: "First line.\nSecond line of the same paragraph.".to_string(),
385396
};
386397
let prompt = module_prompt("src", &[child], &[], &[], &[]);
387-
assert!(prompt.contains("- src/lib.rs: First line. Second line of the same paragraph.\n"));
398+
assert!(
399+
prompt.contains("| src/lib.rs | First line. Second line of the same paragraph. |\n")
400+
);
401+
}
402+
403+
#[test]
404+
fn aggregate_prompts_request_tables_for_enumerable_facts() {
405+
let child = ChildSummary {
406+
name: "src/cli.rs".to_string(),
407+
summary: "Defines commands and config keys.".to_string(),
408+
};
409+
let prompt = repo_prompt(&[child], &[], &[]);
410+
411+
assert!(MODULE_SYSTEM.contains("compact Markdown tables"));
412+
assert!(REPO_SYSTEM.contains("CLI commands or flags"));
413+
assert!(ARCHITECTURE_SYSTEM.contains("public API symbols"));
414+
assert!(prompt.contains("Table guidance:\n"));
415+
assert!(
416+
prompt.contains("configuration keys, environment variables, or public API symbols")
417+
);
418+
assert!(prompt.contains("| Module | Summary |\n| --- | --- |\n"));
419+
}
420+
421+
#[test]
422+
fn file_prompt_lists_symbols_as_markdown_table() {
423+
let symbol = SymbolSummary {
424+
name: "run|cli".to_string(),
425+
kind: "function".to_string(),
426+
component_id: "component|id".to_string(),
427+
component_label: "run [function]".to_string(),
428+
line_start: 7,
429+
line_end: 9,
430+
purpose: "Handles command dispatch.".to_string(),
431+
};
432+
433+
let prompt = file_prompt("src/cli.rs", &[symbol], &[]);
434+
435+
assert!(prompt.contains("| Symbol | Kind | Component | Component ID | Lines | Purpose |"));
436+
assert!(prompt.contains("| run\\|cli | function | run [function] | component\\|id | 7-9 | Handles command dispatch. |"));
388437
}
389438

390439
fn excerpt(path: &str, content: &str) -> SourceExcerpt {

crates/gcode/src/commands/codewiki/render/overview.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,26 @@ pub(crate) fn render_architecture_doc(architecture: &ArchitectureDoc) -> String
2121
}
2222
if !architecture.subsystems.is_empty() {
2323
doc.push_str("## Subsystems\n\n");
24+
write_markdown_table_header(&mut doc, &["Subsystem", "Responsibility", "Child modules"]);
2425
for subsystem in &architecture.subsystems {
25-
let _ = writeln!(
26-
doc,
27-
"- {} - {}",
28-
module_wikilink(&subsystem.module),
29-
subsystem.responsibility
26+
let child_modules = if subsystem.child_modules.is_empty() {
27+
"None".to_string()
28+
} else {
29+
subsystem
30+
.child_modules
31+
.iter()
32+
.map(|child| module_wikilink(child))
33+
.collect::<Vec<_>>()
34+
.join(", ")
35+
};
36+
write_markdown_table_row(
37+
&mut doc,
38+
[
39+
module_wikilink(&subsystem.module),
40+
subsystem.responsibility.clone(),
41+
child_modules,
42+
],
3043
);
31-
// Enumerate one module level below each subsystem so the page
32-
// shows the top one to two levels of the decomposition.
33-
for child in &subsystem.child_modules {
34-
let _ = writeln!(doc, " - {}", module_wikilink(child));
35-
}
3644
}
3745
doc.push('\n');
3846
}

0 commit comments

Comments
 (0)