Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions crates/forge-docs-check/src/extindex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Rule `ext-index`: every `crates/ext_*` extension is listed in the
//! `architecture.md` "Extension Crates" overview table.
//!
//! This is the index/module-list guard (Phase 2.5). It is a **completeness
//! check**, not a generator: the table's `Purpose` column is hand-authored prose
//! and only 13/37 crates carry a `Cargo.toml` description, so generating that
//! column would either fabricate text or replace good labels with inconsistent
//! doc-first-lines. Instead the rule ensures the authored table can never go
//! silently incomplete — which it had (it listed 27 of 37 extensions, missing
//! `console`, `dock`, `encoding`, `image_tools`, `svelte`, `web_inspector`,
//! `codesign`, …). Adding a new extension now requires adding its row, the same
//! way `crate-page` requires its dedicated page.

use crate::checks::read_optional;
use crate::discovery::Workspace;
use crate::Finding;

/// The overview page whose extension table must list every extension crate.
const INDEX_PAGE: &str = "architecture.md";

pub fn check(ws: &Workspace) -> Vec<Finding> {
let page = match read_optional(&ws.docs_dir().join(INDEX_PAGE)) {
Some(p) => p,
None => return Vec::new(),
};
let mut findings = Vec::new();
for krate in ws.extension_crates() {
// The table references each extension by its crate name in a code span,
// e.g. `| `ext_console` | … |`. Require that exact token to be present.
let needle = format!("`{}`", krate.dir_name);
if !page.contains(&needle) {
findings.push(Finding::new(
"ext-index",
format!(
"extension crate `{}` is not listed in {}'s extension overview table",
krate.dir_name, INDEX_PAGE
),
));
}
Comment on lines +28 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current check only verifies if the backticked crate name (e.g., `ext_window`) is present anywhere in the document. However, some extension crates are also mentioned in prose (for example, ext_window is mentioned on line 60 of architecture.md). If such an extension were accidentally removed from the overview table, this check would still pass (a false negative).

To make this check robust, we should verify that the crate name is actually listed within a markdown table row (i.e., a line starting with |).

        // The table references each extension by its crate name in a code span
        // within a table row. To avoid false positives from prose mentions,
        // we require a table row (starting with `|`) containing the backticked crate name.
        let needle = format!("`{}`", krate.dir_name);
        let in_table = page.lines().any(|line| {
            let trimmed = line.trim();
            trimmed.starts_with('|') && trimmed.contains(&needle)
        });
        if !in_table {
            findings.push(Finding::new(
                "ext-index",
                format!(
                    "extension crate `{}` is not listed in {}'s extension overview table",
                    krate.dir_name, INDEX_PAGE
                ),
            ));
        }

}
findings
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;

fn ws_with(arch: &str, ext_crates: &[&str]) -> (tempfile::TempDir, Workspace) {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join("sdk")).unwrap();
fs::create_dir_all(root.join("site/src/content/docs")).unwrap();
fs::write(root.join("site/src/content/docs/architecture.md"), arch).unwrap();
let mut members = String::new();
for c in ext_crates {
let dir = root.join("crates").join(c);
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("Cargo.toml"),
format!("[package]\nname = \"{c}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
)
.unwrap();
members.push_str(&format!(" \"crates/{c}\",\n"));
}
fs::write(
root.join("Cargo.toml"),
format!("[workspace]\nmembers = [\n{members}]\nresolver = \"2\"\n"),
)
.unwrap();
let ws = Workspace::discover_at(root).unwrap();
(tmp, ws)
}

#[test]
fn flags_extension_missing_from_table() {
let arch = "| `ext_fs` | runtime:fs | files |\n";
let (_t, ws) = ws_with(arch, &["ext_fs", "ext_console"]);
let findings = check(&ws);
assert!(
findings.iter().any(|f| f.message.contains("ext_console")),
"missing ext_console must be flagged: {:?}",
findings.iter().map(|f| &f.message).collect::<Vec<_>>()
);
assert!(!findings.iter().any(|f| f.message.contains("ext_fs")));
}

#[test]
fn passes_when_all_listed() {
let arch = "| `ext_fs` | x |\n| `ext_console` | y |\n";
let (_t, ws) = ws_with(arch, &["ext_fs", "ext_console"]);
assert!(check(&ws).is_empty());
}
}
2 changes: 2 additions & 0 deletions crates/forge-docs-check/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod checks;
pub mod cratepage;
pub mod discovery;
pub mod exampleblock;
pub mod extindex;
pub mod markers;

use discovery::Workspace;
Expand Down Expand Up @@ -93,5 +94,6 @@ pub fn run_all_checks(ws: &Workspace) -> Report {
report.extend(checks::forge_docs::check(ws));
report.extend(apiblock::check(ws));
report.extend(exampleblock::check(ws));
report.extend(extindex::check(ws));
report
}
8 changes: 8 additions & 0 deletions site/src/content/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,13 +563,21 @@ All extension crates use forge-weld macros for automatic TypeScript SDK generati
| `ext_protocol` | `runtime:protocol` | Custom protocol handlers |
| `ext_os_compat` | `runtime:os_compat` | OS compatibility layer |
| `ext_debugger` | `runtime:debugger` | Debugging support |
| `ext_console` | `runtime:console` | Console output capture (Deno + renderer) |
| `ext_dock` | `runtime:dock` | macOS dock customization (badge, bounce, menu) |
| `ext_encoding` | `runtime:encoding` | Text encoding/decoding |
| `ext_image_tools` | `runtime:image_tools` | Image conversion (PNG, SVG, WebP, ICO) |
| `ext_svelte` | `runtime:svelte` | SvelteKit integration |
| `ext_web_inspector` | `runtime:web_inspector` | Chrome DevTools Protocol bridge |
| `ext_codesign` | `runtime:codesign` | Code signing (macOS / Windows / Linux) |

### Forge Tool Extension Crates

| Crate | Module | Purpose |
|-------|--------|---------|
| `ext_weld` | `forge:weld` | Runtime code generation, TypeScript transpilation |
| `ext_bundler` | `forge:bundler` | Icon management, manifest parsing, bundling utilities |
| `ext_etcher` | `forge:etcher` | Documentation generation (forge-etch access) |

---

Expand Down
Loading