Skip to content

Commit df48561

Browse files
[gobby-cli-#1007] feat: per-category render versions for codewiki
Replace the single global CODEWIKI_RENDER_VERSION with per-category constants (file, module, repo, architecture, infrastructure, features, deprecations, misc, curated, changes) so a template change in one renderer only invalidates that category's pages instead of forcing a full wiki regeneration. All categories start at 20 for backward compat with existing _meta/codewiki.json. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent 1b56add commit df48561

5 files changed

Lines changed: 171 additions & 9 deletions

File tree

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ impl<'a> DocSink<'a> {
195195
&& meta.ai_route == ai_outcome.route_label()
196196
&& meta.ai_fallback == ai_outcome.fallback
197197
&& meta.ai_generation_status == ai_outcome.status.as_str()
198-
&& meta.render_version == CODEWIKI_RENDER_VERSION
198+
&& meta.render_version == render_version_for_path(&doc.path)
199199
&& !meta.source_hashes.is_empty()
200200
&& (doc.summary.is_none() || meta.summary.is_some())
201201
&& meta
@@ -236,7 +236,7 @@ impl<'a> DocSink<'a> {
236236
&& meta.ai_route == ai_outcome.route_label()
237237
&& meta.ai_fallback == ai_outcome.fallback
238238
&& meta.ai_generation_status == ai_outcome.status.as_str()
239-
&& meta.render_version == CODEWIKI_RENDER_VERSION
239+
&& meta.render_version == render_version_for_path(&doc.path)
240240
&& match &doc.invalidation_key {
241241
Some(key) => {
242242
meta.invalidation_key.as_deref() == Some(key.as_str())
@@ -267,7 +267,7 @@ impl<'a> DocSink<'a> {
267267
&& meta.ai_route == ai_outcome.route_label()
268268
&& meta.ai_fallback == ai_outcome.fallback
269269
&& meta.ai_generation_status == ai_outcome.status.as_str()
270-
&& meta.render_version == CODEWIKI_RENDER_VERSION
270+
&& meta.render_version == render_version_for_path(&doc.path)
271271
&& source_hash_key_sets_match(&meta.source_hashes, &source_hashes)
272272
&& source_hash_key_sets_match(&meta.neighbor_hashes, &neighbor_hashes)
273273
&& (doc.summary.is_none() || meta.summary.is_some())
@@ -310,7 +310,7 @@ impl<'a> DocSink<'a> {
310310
ai_route: write_outcome.route_label().to_string(),
311311
ai_fallback: write_outcome.fallback,
312312
ai_generation_status: write_outcome.status.as_str().to_string(),
313-
render_version: CODEWIKI_RENDER_VERSION,
313+
render_version: render_version_for_path(&doc.path),
314314
neighbor_hashes,
315315
invalidation_key: doc.invalidation_key.clone(),
316316
lane: lane.lane,

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,55 @@ const MAX_EDGE_LIMIT: usize = 100_000;
6161
// Workflows/Getting Started/Operations/Data Model/CLI-API/Troubleshooting),
6262
// semantic cross-directory concept-cluster names, and an enumerable `Reference |
6363
// Summary` table on curated pages, so prior narrative/concept pages re-render.
64-
const CODEWIKI_RENDER_VERSION: u32 = 20;
64+
//
65+
// Per-category render versions (#1007): the single global version was replaced
66+
// by per-category constants so a template change in one renderer only
67+
// invalidates the pages it affects. All categories start at 20 (the prior
68+
// global value) for backward compatibility with existing _meta/codewiki.json.
69+
const RENDER_VERSION_DEFAULT: u32 = 20;
70+
const RENDER_VERSION_FILE: u32 = 20;
71+
const RENDER_VERSION_MODULE: u32 = 20;
72+
const RENDER_VERSION_REPO: u32 = 20;
73+
const RENDER_VERSION_ARCHITECTURE: u32 = 20;
74+
const RENDER_VERSION_INFRASTRUCTURE: u32 = 20;
75+
const RENDER_VERSION_FEATURES: u32 = 20;
76+
const RENDER_VERSION_DEPRECATIONS: u32 = 20;
77+
const RENDER_VERSION_MISC: u32 = 20;
78+
const RENDER_VERSION_CURATED: u32 = 20;
79+
const RENDER_VERSION_CHANGES: u32 = 20;
80+
81+
/// Returns the render-version constant for a doc page path. Each page category
82+
/// (file docs, module docs, architecture, curated narrative, etc.) has its own
83+
/// version so a template change in one renderer only invalidates the pages it
84+
/// affects, instead of forcing a full wiki regeneration.
85+
pub(crate) fn render_version_for_path(path: &str) -> u32 {
86+
if path.starts_with("code/files/") {
87+
RENDER_VERSION_FILE
88+
} else if path.starts_with("code/modules/") {
89+
RENDER_VERSION_MODULE
90+
} else if path.starts_with("code/concepts/") || path.starts_with("code/narrative/") {
91+
RENDER_VERSION_CURATED
92+
} else if path == "code/repo.md" {
93+
RENDER_VERSION_REPO
94+
} else if path == "code/_architecture.md" {
95+
RENDER_VERSION_ARCHITECTURE
96+
} else if path == "code/infrastructure.md" {
97+
RENDER_VERSION_INFRASTRUCTURE
98+
} else if path == "code/features.md" {
99+
RENDER_VERSION_FEATURES
100+
} else if path == "code/deprecations.md" {
101+
RENDER_VERSION_DEPRECATIONS
102+
} else if path == "code/_changes.md" {
103+
RENDER_VERSION_CHANGES
104+
} else if path == "code/_onboarding.md"
105+
|| path == "code/_hotspots.md"
106+
|| path == "code/_ownership.md"
107+
{
108+
RENDER_VERSION_MISC
109+
} else {
110+
RENDER_VERSION_DEFAULT
111+
}
112+
}
65113

66114
/// Default daemon feature profile for the grounded verification pass (#904):
67115
/// `feature_mid` (sonnet) runs the "is this claim supported by the cited

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
22
use std::path::{Path, PathBuf};
33

44
use super::io::{read_codewiki_meta, safe_doc_path};
5-
use super::{BuiltDoc, CODEWIKI_RENDER_VERSION, CodewikiAiOutcome, CodewikiDocMeta, SourceSpan};
5+
use super::{BuiltDoc, CodewikiAiOutcome, CodewikiDocMeta, SourceSpan, render_version_for_path};
66
use crate::index::hasher;
77

88
/// Decides whether a doc's previous content can be reused without any LLM
@@ -124,7 +124,7 @@ impl ReusePlan {
124124
if entry.degraded
125125
|| entry.ai_mode != self.ai_mode
126126
|| !entry_matches_ai_outcome(entry, ai_outcome)
127-
|| entry.render_version != CODEWIKI_RENDER_VERSION
127+
|| entry.render_version != render_version_for_path(doc_path)
128128
|| entry.invalidation_key.as_deref() != Some(invalidation_key)
129129
{
130130
return None;
@@ -235,7 +235,7 @@ impl ReusePlan {
235235
if entry.degraded
236236
|| entry.ai_mode != self.ai_mode
237237
|| !entry_matches_ai_outcome(entry, ai_outcome)
238-
|| entry.render_version != CODEWIKI_RENDER_VERSION
238+
|| entry.render_version != render_version_for_path(doc_path)
239239
|| entry.source_hashes.is_empty()
240240
{
241241
return false;

crates/gcode/src/commands/codewiki/tests/reuse.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,117 @@ fn stale_render_version_disables_reuse() {
192192
);
193193
}
194194

195+
#[test]
196+
fn per_category_render_version_isolates_invalidation() {
197+
let (project, input) = reuse_project();
198+
let out_dir = project.path().join("codewiki");
199+
200+
let mut first_generator = |_prompt: &str, system: &str, _tier: PromptTier| {
201+
if system == prompts::CURATED_NAVIGATION_SYSTEM {
202+
Some(test_curated_navigation_json())
203+
} else if system == prompts::CONCEPT_PAGE_SYSTEM {
204+
Some(test_concept_handbook_body())
205+
} else if system == prompts::NARRATIVE_PAGE_SYSTEM {
206+
Some(test_narrative_handbook_body())
207+
} else {
208+
Some("Generated prose.".to_string())
209+
}
210+
};
211+
let mut progress = CodewikiProgress::silent();
212+
let first = generate_hierarchical_docs_with_progress(
213+
&input,
214+
Some(&mut first_generator),
215+
AiDepth::Symbols,
216+
&mut progress,
217+
);
218+
write_incremental_doc_set_with_snapshot(
219+
project.path(),
220+
&out_dir,
221+
&first,
222+
None,
223+
"symbols",
224+
DocPruneScope::unscoped(),
225+
)
226+
.expect("first write");
227+
228+
// Stale only the architecture page's render version. Every other category
229+
// keeps version 20 and must reuse; only code/_architecture.md regenerates.
230+
let meta_path = out_dir.join("_meta/codewiki.json");
231+
let raw_meta = std::fs::read_to_string(&meta_path).expect("read meta");
232+
let mut meta: serde_json::Value = serde_json::from_str(&raw_meta).expect("parse meta");
233+
if let Some(arch) = meta["docs"]
234+
.as_object_mut()
235+
.expect("docs object")
236+
.get_mut("code/_architecture.md")
237+
{
238+
arch["render_version"] = serde_json::json!(1);
239+
}
240+
std::fs::write(
241+
&meta_path,
242+
format!(
243+
"{}\n",
244+
serde_json::to_string_pretty(&meta).expect("serialize meta")
245+
),
246+
)
247+
.expect("write stale meta");
248+
249+
let mut regenerated_paths = Vec::new();
250+
let mut second_generator = |_prompt: &str, system: &str, _tier: PromptTier| {
251+
if system == prompts::CURATED_NAVIGATION_SYSTEM {
252+
Some(test_curated_navigation_json())
253+
} else if system == prompts::CONCEPT_PAGE_SYSTEM {
254+
Some(test_concept_handbook_body())
255+
} else if system == prompts::NARRATIVE_PAGE_SYSTEM {
256+
Some(test_narrative_handbook_body())
257+
} else {
258+
Some("Regenerated prose.".to_string())
259+
}
260+
};
261+
let mut plan = ReusePlan::load(project.path(), &out_dir, "symbols").expect("reuse plan loads");
262+
let mut reuse = Some(&mut plan);
263+
let mut progress = CodewikiProgress::silent();
264+
let second = generate_hierarchical_docs_with_reuse(
265+
&input,
266+
Some(&mut second_generator),
267+
AiDepth::Symbols,
268+
&mut reuse,
269+
&mut progress,
270+
);
271+
272+
// Collect paths whose content changed (regenerated, not reused).
273+
for doc in &second {
274+
let prev = first.iter().find(|d| d.path == doc.path);
275+
if prev.is_none_or(|p| p.content != doc.content) {
276+
regenerated_paths.push(doc.path.as_str());
277+
}
278+
}
279+
280+
// Architecture must regenerate.
281+
assert!(
282+
regenerated_paths.contains(&"code/_architecture.md"),
283+
"architecture page must regenerate when its render version is stale, got: {regenerated_paths:?}"
284+
);
285+
286+
// File docs and module docs must NOT regenerate — their render versions are
287+
// still valid.
288+
let file_or_module_regen = regenerated_paths
289+
.iter()
290+
.any(|p| p.starts_with("code/files/") || p.starts_with("code/modules/"));
291+
assert!(
292+
!file_or_module_regen,
293+
"file/module pages must reuse when only architecture render version is stale, regenerated: {regenerated_paths:?}"
294+
);
295+
296+
// Curated pages must NOT regenerate.
297+
let curated_regen = regenerated_paths
298+
.iter()
299+
.any(|p| p.starts_with("code/concepts/") || p.starts_with("code/narrative/"));
300+
assert!(
301+
!curated_regen,
302+
"curated pages must reuse when only architecture render version is stale, regenerated: {regenerated_paths:?}"
303+
);
304+
}
305+
195306
#[test]
196307
fn reused_docs_feed_recorded_summaries_into_parent_prompts() {
197308
let (project, input) = reuse_project();

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,10 @@ pub(crate) struct CodewikiDocMeta {
580580
#[serde(default, skip_serializing_if = "String::is_empty")]
581581
pub(crate) ai_generation_status: String,
582582
/// Render-template version for deterministic markdown emitted after model
583-
/// generation. Missing versions force a one-time rewrite on upgrade.
583+
/// generation. Per-category: each page type (file, module, architecture,
584+
/// curated, etc.) has its own version constant so a template change in one
585+
/// renderer only invalidates that category's pages (#1007). Missing or
586+
/// stale versions force a rewrite of the affected category only.
584587
#[serde(default)]
585588
pub(crate) render_version: u32,
586589
/// Cross-file neighbor source hashes (#885, Leaf H). A source-file page

0 commit comments

Comments
 (0)