Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ec67076
feat(C/N2): shared declarative TikZ library catalogue and evidence API
i-shutov Oct 31, 2026
c3ee62a
docs(C/N2): render Markdown links and inline code correctly
i-shutov Oct 31, 2026
23003da
fix(C/N2): apply hosted rustfmt diff and record real XeLaTeX example QA
i-shutov Oct 31, 2026
021ed1a
test(C/N2): compare Windows CRLF fixture as source-equivalent LF and …
i-shutov Oct 31, 2026
fd8e624
fix(C/N2): unknown alternative-engine compatibility is not incompatib…
i-shutov Oct 31, 2026
0ad8978
merge(C/N2): reconcile current main catalogue QA CI without overlap
i-shutov Oct 31, 2026
3b13ac0
fix(C/N2): do not claim libraries absent when document body can impor…
i-shutov Oct 31, 2026
5973fa7
test(C/N2): add genuine XeLaTeX document-body calc import fixture
i-shutov Oct 31, 2026
a792f66
test(C/N2): late/imported dependencies, source refusal, CRLF and lexi…
i-shutov Oct 31, 2026
4a2a756
docs(C/N2): explain late imports and UNKNOWN source authority contract
i-shutov Oct 31, 2026
3375645
plan(C/N2): close real late-library source-evidence regression
i-shutov Oct 31, 2026
9045b56
insight(C/N2): record verified XeLaTeX bug fixture and pending Rust CI
i-shutov Oct 31, 2026
6e3173e
style(C/N2): apply exact hosted rustfmt diff without suppressions
i-shutov Oct 31, 2026
1adb5aa
qa(C/N2): preserve failed initial rustfmt provenance and exact correc…
i-shutov Oct 31, 2026
cba46ec
docs(C/N2): align recognizer comment with conservative body hint dete…
i-shutov Oct 31, 2026
731c656
fix(C/N2): satisfy strict Clippy collapsible-if without lint suppression
i-shutov Oct 31, 2026
e66ed1f
qa(C/N2): record exact Clippy failure/fix and negative XeLaTeX control
i-shutov Oct 31, 2026
7114d2d
feat(C/N2): consolidate shared library catalogue with late-import sou…
i-shutov Oct 31, 2026
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
16 changes: 16 additions & 0 deletions crates/tikzarium-extensions/src/bin/n2-library-fixture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! C-owned generated TeX documents for independent N2 XeLaTeX verification.
//! Usage: cargo run -p tikzarium-extensions --bin n2-library-fixture -- calc
use std::{env, process};
use tikzarium_extensions::primitives::Catalog;

fn main() {
let name = env::args().nth(1).unwrap_or_default();
let catalog = Catalog::bundled().expect("valid bundled declarative catalogue");
match catalog.library(&name) {
Some(library) => print!("{}", library.example),
None => {
eprintln!("unknown N2 library; select arrows.meta, positioning, calc or fit");
process::exit(2);
}
}
}
66 changes: 66 additions & 0 deletions crates/tikzarium-extensions/src/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
};
use thiserror::Error;
pub const SCHEMA_VERSION: u32 = 1;
pub const N2_LIBRARY_NAMES: [&str; 4] = ["arrows.meta", "positioning", "calc", "fit"];
const IDS: [&str; 6] = [
"tikz.line",
"tikz.arrow",
Expand All @@ -23,6 +24,8 @@ pub enum PrimitiveError {
Toml(#[from] toml::de::Error),
#[error("Unknown tool: {0}")]
UnknownTool(String),
#[error("Unknown TikZ library: {0}")]
UnknownLibrary(String),
#[error("Unknown parameter: {0}")]
UnknownParameter(String),
#[error("Missing parameter: {0}")]
Expand All @@ -41,6 +44,8 @@ pub struct Catalog {
pub schema_version: u32,
pub id: String,
pub tools: Vec<ToolSpec>,
#[serde(default)]
pub libraries: Vec<LibrarySpec>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
Expand All @@ -59,6 +64,21 @@ pub struct ToolSpec {
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LibrarySpec {
pub id: String,
pub name: String,
pub title_ru: String,
pub syntax: String,
pub help_ru: String,
pub example: String,
pub warning_ru: String,
pub interaction_ru: String,
pub manual_section: String,
pub manual_url: String,
pub requirements: Requirements,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Requirements {
pub packages: Vec<String>,
pub libraries: Vec<String>,
Expand Down Expand Up @@ -119,6 +139,10 @@ impl Catalog {
pub fn tool(&self, id: &str) -> Option<&ToolSpec> {
self.tools.iter().find(|t| t.id == id)
}
/// Declarative N2 help/requirements only; not proof of installed packages.
pub fn library(&self, name: &str) -> Option<&LibrarySpec> {
self.libraries.iter().find(|library| library.name == name)
}
/// Installation status and project-loaded status are independent observations.
pub fn capabilities(
&self,
Expand Down Expand Up @@ -296,6 +320,48 @@ impl Catalog {
return Err(invalid("example mismatches executable schema"));
}
}
if self.libraries.len() != N2_LIBRARY_NAMES.len() {
return Err(invalid("incomplete N2 TikZ library help"));
}
let mut seen_libraries = HashSet::new();
for library in &self.libraries {
let expected_syntax = format!("\\usetikzlibrary{{{}}}", library.name);
let expected_id = format!("tikz.library.{}", library.name.replace('.', "-"));
if !N2_LIBRARY_NAMES.contains(&library.name.as_str())
|| !seen_libraries.insert(library.name.as_str())
|| library.id != expected_id
|| library.syntax != expected_syntax
|| library.requirements.packages != ["tikz"]
|| library.requirements.libraries != [library.name.as_str()]
|| library.requirements.engine != "xelatex"
|| library.requirements.pgf_version_min != "UNKNOWN"
|| library.requirements.documented_against != "3.1.12"
|| [
library.title_ru.as_str(),
&library.help_ru,
&library.warning_ru,
&library.interaction_ru,
&library.manual_section,
]
.iter()
.any(|value| value.trim().is_empty())
|| !library.manual_url.starts_with("https://tikz.dev/")
|| !library.example.contains("\\usepackage{tikz}")
|| !library.example.contains(&expected_syntax)
|| !library.example.contains("\\begin{document}")
|| !library.example.contains("\\end{document}")
{
return Err(invalid("invalid or incomplete N2 library record"));
}
}
if self.tools.iter().any(|tool| {
tool.requirements
.libraries
.iter()
.any(|name| !self.libraries.iter().any(|library| library.name == *name))
}) {
return Err(invalid("tool requirement lacks shared library help"));
}
Ok(())
}
}
Expand Down
5 changes: 2 additions & 3 deletions crates/tikzarium-extensions/src/project_disconnect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

use crate::{
primitive_edit::SourceTarget,
primitives::{Evidence, PrimitiveError, SourcePatch},
primitives::{Evidence, N2_LIBRARY_NAMES, PrimitiveError, SourcePatch},
project_imports::{ImportKind, inspect, literal_names, uncommented},
};
use std::ops::Range;

const BEGIN_DOCUMENT: &str = "\\begin{document}";
const SUPPORTED_LIBRARIES: [&str; 4] = ["arrows.meta", "positioning", "calc", "fit"];

/// A proposed, source-visible deletion. B owns confirmation and Undo UI.
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -140,7 +139,7 @@ pub fn plan_library_disconnect(
target: SourceTarget,
name: &str,
) -> DisconnectDecision {
if !SUPPORTED_LIBRARIES.contains(&name) {
if !N2_LIBRARY_NAMES.contains(&name) {
return DisconnectDecision::Refused {
reason: "library has no N2 disconnect adapter; use source",
};
Expand Down
113 changes: 109 additions & 4 deletions crates/tikzarium-extensions/src/project_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@

use crate::{
primitive_edit::SourceTarget,
primitives::{Capabilities, Catalog, Evidence, PrimitiveError, SourcePatch},
primitives::{Capabilities, Catalog, Evidence, N2_LIBRARY_NAMES, PrimitiveError, SourcePatch},
};
use std::{collections::BTreeSet, ops::Range};

const BEGIN_DOCUMENT: &str = "\\begin{document}";
const SUPPORTED_LIBRARIES: [&str; 4] = ["arrows.meta", "positioning", "calc", "fit"];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportKind {
Expand Down Expand Up @@ -239,8 +238,47 @@ fn literal_flat_font_assignment(line: &str, command: &str) -> bool {
.all(|b| b.is_ascii_alphanumeric() || b" ._+-".contains(&b))
}

/// A *hint* that source after \begin{document} might import packages or
/// libraries. This never grants Confirmed evidence: verbatim and macro scopes
/// cannot be resolved by a lexical scanner. False positives fail closed.
fn contains_control_word(line: &str, command: &str) -> bool {
line.match_indices(command).any(|(start, _)| {
// A preceding escaped backslash is a TeX \\ control symbol, not
// the start of this control word. Handle longer backslash runs too.
let previous_slashes = line[..start]
.bytes()
.rev()
.take_while(|byte| *byte == b'\\')
.count();
previous_slashes.is_multiple_of(2)
&& !line[start + command.len()..]
.chars()
.next()
.is_some_and(|ch| ch.is_alphabetic() || ch == '@')
})
}

fn document_body_may_import(body: &str) -> bool {
const IMPORT_LIKE_COMMANDS: [&str; 6] = [
"\\usetikzlibrary",
"\\usepackage",
"\\RequirePackage",
"\\input",
"\\include",
"\\csname",
];
body.split_inclusive('\n').any(|raw| {
let line = raw.trim_end_matches(['\r', '\n']);
let active = uncommented(line);
IMPORT_LIKE_COMMANDS
.iter()
.any(|command| contains_control_word(active, command))
})
}

/// Recognize only complete, literal preamble lines. Never expand arbitrary macros.
/// Text after begin(document) is left entirely opaque to this scanner.
/// After begin(document), detect only possible late import hints: these never
/// establish positive evidence or authorize a source edit.
#[must_use]
pub fn inspect(source: &str) -> ProjectImports {
let has_crlf = source.contains("\r\n");
Expand Down Expand Up @@ -402,6 +440,20 @@ pub fn inspect(source: &str) -> ProjectImports {
}
offset += raw.len();
}
// Direct library loading after \begin{document} is supported by TeX.
// Likewise, body-side \input/\include can pull in external imports. Do
// not turn a preamble-only absence into a false NOT_LOADED verdict or
// silently add an earlier duplicate. Existing trusted sites stay known.
if result
.begin_document
.as_ref()
.is_some_and(|begin| document_body_may_import(&source[begin.end..]))
{
first_reason(
&mut result.uncertain_reason,
"possible late import or external source in document body",
);
}
if result.begin_document.is_none() {
first_reason(
&mut result.uncertain_reason,
Expand Down Expand Up @@ -446,6 +498,59 @@ pub fn capabilities_for_tool(
catalog.capabilities(tool_id, installed, loaded, engine)
}

/// N2 library panel status from the *same* declarative catalogue as the six
/// primitive tools. Installation is caller-provided, never inferred from TeX
/// declarations; visual_editable stays false for arbitrary library constructs.
/// tool_implemented means at least one listed native generator depends on this
/// library (currently arrows.meta only), not full library editing coverage.
pub fn capabilities_for_library(
catalog: &Catalog,
name: &str,
project: &ProjectImports,
installed: Evidence,
engine: Option<&str>,
) -> Result<Capabilities, PrimitiveError> {
let library = catalog
.library(name)
.ok_or_else(|| PrimitiveError::UnknownLibrary(name.to_owned()))?;
let package = project.status(ImportKind::Package, "tikz");
let loaded_library = project.status(ImportKind::TikzLibrary, name);
let project_loaded = if package == Evidence::Confirmed && loaded_library == Evidence::Confirmed
{
Evidence::Confirmed
} else if package == Evidence::Unknown || loaded_library == Evidence::Unknown {
Evidence::Unknown
} else {
Evidence::Absent
};
let tool_implemented = if catalog.tools.iter().any(|tool| {
tool.requirements
.libraries
.iter()
.any(|required| required == name)
}) {
Evidence::Confirmed
} else {
Evidence::Absent
};
let engine_compatible = match engine {
Some(actual) if actual.eq_ignore_ascii_case(&library.requirements.engine) => {
Evidence::Confirmed
}
// The catalogue records an XeLaTeX proof target, not a claim that
// other engines reject this PGF/TikZ library.
_ => Evidence::Unknown,
};
Ok(Capabilities {
installed,
project_loaded,
engine_compatible,
tool_implemented,
help_available: Evidence::Confirmed,
visual_editable: false,
})
}

/// An explicit source proposal; B must show the diff and request the user action.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportPlan {
Expand Down Expand Up @@ -492,7 +597,7 @@ pub fn plan_import(
reason: "only the tikz package has an N2 automatic source adapter",
};
}
ImportKind::TikzLibrary if !SUPPORTED_LIBRARIES.contains(&name) => {
ImportKind::TikzLibrary if !N2_LIBRARY_NAMES.contains(&name) => {
return ImportDecision::Refused {
reason: "library has no N2 literal import adapter; use source",
};
Expand Down
Loading
Loading