Use configured languages in generation validation
This commit is contained in:
@@ -20,7 +20,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
||||
- Local MCP automation over stdio or a localhost-only stateless HTTP endpoint, with project resources, read/search/count tools, inert write proposals, explicit desktop approval, and opt-in Claude Code/Copilot configuration.
|
||||
- A fully localized Ratatui terminal workspace, available locally through `bds-cli tui`/`BDS_MODE=tui` and remotely through authenticated SSH shell sessions, with shared post/template/script editing and publishing, project/search/command overlays, settings, tags, Git, reports, task progress, live multi-client locale updates, and airplane-mode AI gating.
|
||||
- `bds-cli server` hosting the shared application engines over a loopback-by-default, public-key-only SSH service, with restrictive private key material, live authorization updates, terminal-session transport, CLI-change synchronization, ordered domain/task events, and native desktop remote-project selection.
|
||||
- bDS2-compatible Markdown/Liquid rendering with built-in macros rendered from bundled Liquid templates in isolated scopes (customizable with `macros/*` partial-template slugs), descriptive category archive titles, canonical multilingual and flat page routes, recursive menus, calendar archives, feeds, a root hreflang sitemap, Pagefind, and incremental site generation through cancellable section task groups.
|
||||
- bDS2-compatible Markdown/Liquid rendering with built-in macros rendered from bundled Liquid templates in isolated scopes (customizable with `macros/*` partial-template slugs), descriptive category archive titles, canonical multilingual and flat page routes for every configured blog language, recursive menus, calendar archives, feeds, a root hreflang sitemap, Pagefind, and incremental site generation through cancellable section task groups.
|
||||
- Navigable generated-route preview in the app or system browser, with draft database overlays and published filesystem content.
|
||||
- Optional one-shot AI translation, description, analysis, taxonomy, and language-detection operations run in background tasks with editor-level waiting indicators, using provider-portable JSON-only requests through independent online and local OpenAI-compatible profiles. Each profile has secure credentials, persistently discovered chat/title/image model selections, explicit tool/vision overrides, chat testing, and restart-persistent status-bar airplane-mode routing.
|
||||
- Persistent conversational AI with safe Markdown, streamed and cancellable responses, model/session/token tracking, bounded project-aware blog tools, and localized conversation management in the Chat workspace. Allowlisted render tools add persistent native cards, charts, forms, lists, metrics, mind maps, tables, and tabs without executing assistant-provided HTML or JavaScript.
|
||||
|
||||
@@ -498,7 +498,7 @@ fn render(db: &Database, incremental: bool, force: bool) -> Result<CommandOutput
|
||||
std::fs::create_dir_all(&output_dir)?;
|
||||
if incremental {
|
||||
let validation = engine::validate_site::validate_site(db.conn(), &data_dir, &project.id)?;
|
||||
let sections = engine::generation::sections_from_validation_report(&validation);
|
||||
let sections = engine::generation::sections_from_validation_report(&validation, &metadata);
|
||||
let report = engine::generation::apply_validation_sections(
|
||||
db.conn(),
|
||||
&output_dir,
|
||||
|
||||
@@ -210,7 +210,10 @@ pub fn render_site_section_with_progress(
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
pub fn sections_from_validation_report(report: &SiteValidationReport) -> Vec<GenerationSection> {
|
||||
pub fn sections_from_validation_report(
|
||||
report: &SiteValidationReport,
|
||||
metadata: &ProjectMetadata,
|
||||
) -> Vec<GenerationSection> {
|
||||
let mut sections = HashSet::new();
|
||||
let mut saw_unknown = false;
|
||||
|
||||
@@ -220,7 +223,7 @@ pub fn sections_from_validation_report(report: &SiteValidationReport) -> Vec<Gen
|
||||
.chain(report.extra_pages.iter())
|
||||
.chain(report.stale_pages.iter())
|
||||
{
|
||||
match classify_generated_path(path) {
|
||||
match classify_generated_path(path, metadata) {
|
||||
Some(section) => {
|
||||
sections.insert(section);
|
||||
}
|
||||
@@ -276,7 +279,13 @@ pub fn apply_validation_sections(
|
||||
)?);
|
||||
}
|
||||
|
||||
remove_extra_section_paths(output_dir, &expected_paths, §ion_set, &mut report)?;
|
||||
remove_extra_section_paths(
|
||||
output_dir,
|
||||
&expected_paths,
|
||||
metadata,
|
||||
§ion_set,
|
||||
&mut report,
|
||||
)?;
|
||||
report.append(build_site_search_index(
|
||||
conn, output_dir, project_id, metadata,
|
||||
)?);
|
||||
@@ -318,7 +327,7 @@ pub fn apply_validation_section_with_progress(
|
||||
.iter()
|
||||
.chain(validation.extra_pages.iter())
|
||||
.chain(validation.stale_pages.iter())
|
||||
.any(|path| classify_generated_path(path).is_none());
|
||||
.any(|path| classify_generated_path(path, metadata).is_none());
|
||||
let artifacts = if fallback {
|
||||
build_site_section_render_artifacts(
|
||||
conn,
|
||||
@@ -376,7 +385,7 @@ pub fn apply_validation_section_with_progress(
|
||||
if is_cancelled() {
|
||||
return Err(EngineError::Validation("cancelled".to_string()));
|
||||
}
|
||||
let owned_by_section = classify_generated_path(path)
|
||||
let owned_by_section = classify_generated_path(path, metadata)
|
||||
.map_or(section == GenerationSection::Core, |owner| owner == section);
|
||||
if owned_by_section && output_dir.join(path).is_file() {
|
||||
std::fs::remove_file(output_dir.join(path)).map_err(EngineError::Io)?;
|
||||
@@ -680,7 +689,7 @@ fn expected_paths_for_sections(
|
||||
) -> HashSet<String> {
|
||||
let mut expected = pages
|
||||
.iter()
|
||||
.filter(|page| path_matches_sections(&page.relative_path, sections))
|
||||
.filter(|page| path_matches_sections(&page.relative_path, metadata, sections))
|
||||
.map(|page| page.relative_path.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
@@ -710,6 +719,7 @@ fn expected_paths_for_sections(
|
||||
fn remove_extra_section_paths(
|
||||
output_dir: &Path,
|
||||
expected: &HashSet<String>,
|
||||
metadata: &ProjectMetadata,
|
||||
sections: &HashSet<GenerationSection>,
|
||||
report: &mut GenerationReport,
|
||||
) -> EngineResult<()> {
|
||||
@@ -738,7 +748,7 @@ fn remove_extra_section_paths(
|
||||
continue;
|
||||
}
|
||||
if !matches_generated_extension(&rel)
|
||||
|| !path_matches_sections(&rel, sections)
|
||||
|| !path_matches_sections(&rel, metadata, sections)
|
||||
|| expected.contains(&rel)
|
||||
{
|
||||
continue;
|
||||
@@ -752,13 +762,20 @@ fn remove_extra_section_paths(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn path_matches_sections(path: &str, sections: &HashSet<GenerationSection>) -> bool {
|
||||
classify_generated_path(path)
|
||||
fn path_matches_sections(
|
||||
path: &str,
|
||||
metadata: &ProjectMetadata,
|
||||
sections: &HashSet<GenerationSection>,
|
||||
) -> bool {
|
||||
classify_generated_path(path, metadata)
|
||||
.map(|section| sections.contains(§ion))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn classify_generated_path(path: &str) -> Option<GenerationSection> {
|
||||
pub(crate) fn classify_generated_path(
|
||||
path: &str,
|
||||
metadata: &ProjectMetadata,
|
||||
) -> Option<GenerationSection> {
|
||||
if path.ends_with(".xml") || path.ends_with(".json") {
|
||||
return Some(GenerationSection::Core);
|
||||
}
|
||||
@@ -767,7 +784,7 @@ pub(crate) fn classify_generated_path(path: &str) -> Option<GenerationSection> {
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if has_language_prefix(&parts) {
|
||||
if has_language_prefix(&parts, metadata) {
|
||||
parts.remove(0);
|
||||
}
|
||||
|
||||
@@ -805,10 +822,12 @@ pub(crate) fn classify_generated_path(path: &str) -> Option<GenerationSection> {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_language_prefix(parts: &[&str]) -> bool {
|
||||
fn has_language_prefix(parts: &[&str], metadata: &ProjectMetadata) -> bool {
|
||||
match parts {
|
||||
[first, second, ..] => {
|
||||
matches!(*first, "de" | "en" | "es" | "fr" | "it")
|
||||
render_languages(metadata)
|
||||
.iter()
|
||||
.any(|language| language.eq_ignore_ascii_case(first))
|
||||
&& (*second == "index.html"
|
||||
|| *second == "404.html"
|
||||
|| *second == "page"
|
||||
|
||||
@@ -227,7 +227,7 @@ fn build_site_render_artifacts_with_mode(
|
||||
.par_iter()
|
||||
.filter(|route| {
|
||||
section.is_none_or(|section| {
|
||||
classify_generated_path(&route.relative_path) == Some(section)
|
||||
classify_generated_path(&route.relative_path, metadata) == Some(section)
|
||||
}) && requested_paths
|
||||
.is_none_or(|requested| requested.contains(&route.relative_path))
|
||||
})
|
||||
|
||||
@@ -782,7 +782,7 @@ fn apply_validation_repairs_core_section_outputs() {
|
||||
std::fs::remove_file(dir.path().join("atom.xml")).unwrap();
|
||||
|
||||
let report = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
let sections = sections_from_validation_report(&report);
|
||||
let sections = sections_from_validation_report(&report, &metadata);
|
||||
let apply_report =
|
||||
apply_validation_sections(db.conn(), dir.path(), "p1", &metadata, &posts, §ions)
|
||||
.unwrap();
|
||||
@@ -817,7 +817,7 @@ fn apply_validation_removes_extra_section_outputs() {
|
||||
.extra_pages
|
||||
.contains(&"tag/ghost/index.html".to_string())
|
||||
);
|
||||
let sections = sections_from_validation_report(&report);
|
||||
let sections = sections_from_validation_report(&report, &metadata);
|
||||
let apply_report =
|
||||
apply_validation_sections(db.conn(), dir.path(), "p1", &metadata, &posts, §ions)
|
||||
.unwrap();
|
||||
@@ -831,6 +831,59 @@ fn apply_validation_removes_extra_section_outputs() {
|
||||
assert!(repaired.extra_pages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_language_prefixes_select_their_actual_validation_section() {
|
||||
let mut metadata = make_metadata();
|
||||
metadata.blog_languages = vec!["en".into(), "pt".into(), "ja".into()];
|
||||
let report = bds_core::engine::validate_site::SiteValidationReport {
|
||||
missing_pages: vec![
|
||||
"pt/category/ghost/index.html".into(),
|
||||
"ja/category/ghost/index.html".into(),
|
||||
],
|
||||
extra_pages: Vec::new(),
|
||||
stale_pages: Vec::new(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
sections_from_validation_report(&report, &metadata),
|
||||
vec![GenerationSection::Category]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_apply_removes_extra_output_under_configured_language() {
|
||||
let (db, dir) = setup();
|
||||
let mut metadata = make_metadata();
|
||||
metadata.blog_languages = vec!["en".into(), "pt".into(), "ja".into()];
|
||||
bds_core::engine::meta::write_project_json(dir.path(), &metadata).unwrap();
|
||||
let mut post = make_post("hello", 1_710_000_000_000);
|
||||
write_published_snapshot(&dir, &mut post, "Hello **world**");
|
||||
bds_core::db::queries::post::insert_post(db.conn(), &post).unwrap();
|
||||
let posts = vec![PublishedPostSource {
|
||||
post,
|
||||
body_markdown: "Hello **world**".into(),
|
||||
}];
|
||||
|
||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||
let extra = dir.path().join("pt/category/ghost/index.html");
|
||||
std::fs::create_dir_all(extra.parent().unwrap()).unwrap();
|
||||
std::fs::write(&extra, "ghost").unwrap();
|
||||
|
||||
let validation = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
let sections = sections_from_validation_report(&validation, &metadata);
|
||||
let applied =
|
||||
apply_validation_sections(db.conn(), dir.path(), "p1", &metadata, &posts, §ions)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(sections, vec![GenerationSection::Category]);
|
||||
assert!(
|
||||
applied
|
||||
.deleted_paths
|
||||
.contains(&"pt/category/ghost/index.html".to_string())
|
||||
);
|
||||
assert!(!extra.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_validation_uses_html_output_directory_when_present() {
|
||||
let (db, dir) = setup();
|
||||
|
||||
@@ -2965,7 +2965,8 @@ impl TuiApp {
|
||||
engine::validate_site::validate_site(db.conn(), &data_dir, &project_id)?;
|
||||
let metadata = engine::meta::read_project_json(&data_dir)?;
|
||||
let posts = published_sources(db.conn(), &data_dir, &project_id)?;
|
||||
let sections = engine::generation::sections_from_validation_report(&validation);
|
||||
let sections =
|
||||
engine::generation::sections_from_validation_report(&validation, &metadata);
|
||||
let report = engine::generation::apply_validation_sections(
|
||||
db.conn(),
|
||||
&data_dir.join("html"),
|
||||
|
||||
@@ -5355,8 +5355,10 @@ impl BdsApp {
|
||||
extra_pages: self.site_validation_state.extra_files.clone(),
|
||||
stale_pages: self.site_validation_state.stale_files.clone(),
|
||||
};
|
||||
let sections = engine::generation::sections_from_validation_report(&report);
|
||||
if sections.is_empty() {
|
||||
if report.missing_pages.is_empty()
|
||||
&& report.extra_pages.is_empty()
|
||||
&& report.stale_pages.is_empty()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ impl BdsApp {
|
||||
|
||||
let sections = validation.as_ref().map_or_else(
|
||||
|| engine::generation::GenerationSection::ALL.to_vec(),
|
||||
engine::generation::sections_from_validation_report,
|
||||
|validation| engine::generation::sections_from_validation_report(validation, &metadata),
|
||||
);
|
||||
if sections.is_empty() {
|
||||
return Task::none();
|
||||
|
||||
Reference in New Issue
Block a user