pr4xis 0.29.1

Axiomatic Intelligence — an ontology + category-theory reasoning engine: every claim derived from explicit axioms, with a proof path back to them
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
//! XML 1.0 grounding sources → Rust ontology data codegen.
//!
//! Loads two W3C-published files at build time:
//!
//! - **W3C XML namespace XSD** (`https://www.w3.org/2001/xml.xsd`) —
//!   the canonical machine-readable definition of `xml:lang`,
//!   `xml:space`, `xml:base`, `xml:id`. Text-scanned for
//!   `<xs:attribute name="...">` declarations.
//! - **W3C XML Information Set rec** (Cowan & Tobin 2004; XHTML 1.0
//!   Transitional publication format). The 11 information items are
//!   extracted by walking the `<h3><a name="infoitem.<id>">N.N. <Title></a></h3>`
//!   section-heading structure.
//!
//! Per "bottom-up loaded, never encoded": every Tier 2+ ontology
//! type derives from a registered authoritative source, not from
//! Rust enum variants written by hand. Both sources are hash-pinned
//! in `praxis.lock`.
//!
//! ## Citations
//!
//! - **Bray, Hollander, Layman, Tobin & Thompson (eds.) (2009)**
//!   *Namespaces in XML 1.0 (Third Edition)*, W3C Recommendation 8
//!   December 2009. <https://www.w3.org/TR/xml-names/>.
//! - **Bray, Paoli, Sperberg-McQueen, Maler & Yergeau (eds.) (2008)**
//!   *Extensible Markup Language (XML) 1.0 (Fifth Edition)*, W3C
//!   Recommendation 26 November 2008. <https://www.w3.org/TR/xml/>.
//! - **Cowan, J. & Tobin, R. (eds.) (2004)** *XML Information Set
//!   (Second Edition)*, W3C Recommendation 4 February 2004.
//!   <https://www.w3.org/TR/xml-infoset/>.
//! - **Marsh, J. & Tobin, R. (eds.) (2009)** *XML Base (Second
//!   Edition)*, W3C Recommendation 28 January 2009.
//!   <https://www.w3.org/TR/xmlbase/>.

use std::path::Path;

/// Errors returned by the XML-1.0 codegen entry points.
#[derive(Debug)]
pub enum XmlSchemaCodegenError {
    /// Could not read the source file from disk.
    ReadSource(String, std::io::Error),
    /// The expected scan produced zero hits — the source bundle is
    /// broken or the scan logic is out of date.
    EmptyScan(String),
}

impl core::fmt::Display for XmlSchemaCodegenError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::ReadSource(p, e) => write!(f, "read source {p}: {e}"),
            Self::EmptyScan(what) => write!(f, "empty scan for {what}"),
        }
    }
}

impl std::error::Error for XmlSchemaCodegenError {}

// =============================================================================
// xml.xsd → Rust source
// =============================================================================

/// Read `xml.xsd` from disk and emit a Rust source file declaring
/// `pub const XML_NAMESPACE_ATTRIBUTES: &[&str]` with the four
/// reserved attribute names (`base`, `id`, `lang`, `space`) in
/// alphabetical order.
///
/// Suitable for writing into `$OUT_DIR/xml_namespace_schema_generated.rs`
/// from a build script.
///
/// # Errors
///
/// Returns [`XmlSchemaCodegenError::ReadSource`] if the XSD can't be
/// read, or [`XmlSchemaCodegenError::EmptyScan`] if the file
/// contains no `<xs:attribute name=...>` declarations.
pub fn generate_xml_namespace_schema_source(
    xsd_path: &Path,
) -> Result<String, XmlSchemaCodegenError> {
    let xsd = std::fs::read_to_string(xsd_path)
        .map_err(|e| XmlSchemaCodegenError::ReadSource(xsd_path.display().to_string(), e))?;
    generate_xml_namespace_schema_from_source(&xsd)
}

/// Emit the XML namespace schema codegen Rust source from the schema
/// bytes already in memory — the byte-stream sibling of
/// [`generate_xml_namespace_schema_source`] for callers that hold the
/// decoded source (e.g. a build script that has materialized the
/// committed `.prx` envelope rather than reading the raw `.xsd` from
/// disk). The path-reading entry point delegates here, so both share one
/// scan/emit body.
///
/// # Errors
///
/// Returns [`XmlSchemaCodegenError::EmptyScan`] if the source contains no
/// `<xs:attribute name=...>` declarations.
pub fn generate_xml_namespace_schema_from_source(
    xsd: &str,
) -> Result<String, XmlSchemaCodegenError> {
    let names = scan_xsd_attribute_names(xsd);
    if names.is_empty() {
        return Err(XmlSchemaCodegenError::EmptyScan(
            "xml.xsd <xs:attribute name=...>".to_string(),
        ));
    }

    let mut out = String::new();
    out.push_str(
        "// Generated by pr4xis::codegen::xml_schemas::generate_xml_namespace_schema_source.\n",
    );
    out.push_str(
        "// Source: W3C xml.xsd — the W3C-published xml namespace schema.\n\
         // Citation: Bray, Hollander, Layman, Tobin & Thompson (2009) Namespaces in XML 1.0\n\
         // (Third Edition), W3C Recommendation; Bray et al. (2008) XML 1.0 Fifth Edition\n\
         // §2.10 (xml:space) + §2.12 (xml:lang); Marsh & Tobin (2009) XML Base.\n",
    );
    out.push_str(&format!("// Attributes loaded: {}\n", names.len()));
    out.push_str("pub const XML_NAMESPACE_ATTRIBUTES: &[&str] = &[\n");
    for name in &names {
        out.push_str(&format!("    {:?},\n", name));
    }
    out.push_str("];\n");
    Ok(out)
}

/// Scan an XSD source string for `<xs:attribute name="...">`
/// declarations and return the names in alphabetical order. Mirrors
/// the same minimal text-scan that the XHTML loader uses; the XSD's
/// structural well-formedness is a build-time invariant.
fn scan_xsd_attribute_names(xsd: &str) -> Vec<String> {
    const NEEDLE: &str = "<xs:attribute name=\"";
    let mut out = std::collections::BTreeSet::new();
    let mut rest = xsd;
    while let Some(idx) = rest.find(NEEDLE) {
        let after = &rest[idx + NEEDLE.len()..];
        if let Some(end) = after.find('"') {
            out.insert(after[..end].to_string());
            rest = &after[end..];
        } else {
            break;
        }
    }
    out.into_iter().collect()
}

// =============================================================================
// xml-infoset.xhtml → Rust source
// =============================================================================

/// Read the XML Information Set rec (XHTML 1.0 Transitional) from
/// disk and emit a Rust source file declaring
/// `pub const XML_INFOSET_INFORMATION_ITEMS: &[InformationItemEntry]`
/// with the 11 information items in section order. Each entry
/// carries the section number (e.g. `"2.1"`), the section anchor
/// (e.g. `"infoitem.document"`), the canonical English name (e.g.
/// `"Document"`), and the Rust enum-variant identifier (e.g.
/// `"DocumentItem"`).
///
/// Suitable for writing into `$OUT_DIR/xml_infoset_generated.rs`
/// from a build script.
///
/// # Errors
///
/// Returns [`XmlSchemaCodegenError::ReadSource`] if the XHTML file
/// can't be read, or [`XmlSchemaCodegenError::EmptyScan`] if the
/// section-heading scan finds zero information items.
pub fn generate_xml_infoset_source(xhtml_path: &Path) -> Result<String, XmlSchemaCodegenError> {
    let xhtml = std::fs::read_to_string(xhtml_path)
        .map_err(|e| XmlSchemaCodegenError::ReadSource(xhtml_path.display().to_string(), e))?;
    generate_xml_infoset_from_source(&xhtml)
}

/// Emit the XML Information Set codegen Rust source from the XHTML rec
/// bytes already in memory — the byte-stream sibling of
/// [`generate_xml_infoset_source`] for callers that hold the decoded
/// source (e.g. a build script materializing the committed `.prx`
/// envelope rather than reading the raw `.xhtml` from disk). The
/// path-reading entry point delegates here.
///
/// # Errors
///
/// Returns [`XmlSchemaCodegenError::EmptyScan`] if the section-heading
/// scan finds zero information items.
pub fn generate_xml_infoset_from_source(xhtml: &str) -> Result<String, XmlSchemaCodegenError> {
    let items = scan_infoset_items(xhtml);
    if items.is_empty() {
        return Err(XmlSchemaCodegenError::EmptyScan(
            "xml-infoset.xhtml <h3><a name=\"infoitem.*\">...</a></h3>".to_string(),
        ));
    }

    let mut out = String::new();
    out.push_str("// Generated by pr4xis::codegen::xml_schemas::generate_xml_infoset_source.\n");
    out.push_str(
        "// Source: W3C XML Information Set (Second Edition), W3C Recommendation\n\
         // 4 February 2004. Editors: Cowan & Tobin.\n\
         // Information items per §2 — the 11-item conceptual taxonomy.\n",
    );
    out.push_str(&format!("// Items loaded: {}\n", items.len()));
    out.push_str(
        "/// One information item extracted from the published rec's section\n\
         /// hierarchy. `section` is the dotted number (e.g. `\"2.1\"`),\n\
         /// `anchor` is the `<a name=\"...\">` identifier (e.g.\n\
         /// `\"infoitem.document\"`), `english_name` is the canonical\n\
         /// English head-noun phrase (e.g. `\"Document\"`),\n\
         /// `variant_ident` is the Rust enum-variant identifier (e.g.\n\
         /// `\"DocumentItem\"`).\n\
         #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n\
         pub struct InformationItemEntry {\n    \
            pub section: &'static str,\n    \
            pub anchor: &'static str,\n    \
            pub english_name: &'static str,\n    \
            pub variant_ident: &'static str,\n\
         }\n\n",
    );
    out.push_str("pub const XML_INFOSET_INFORMATION_ITEMS: &[InformationItemEntry] = &[\n");
    for it in &items {
        out.push_str(&format!(
            "    InformationItemEntry {{ section: {:?}, anchor: {:?}, english_name: {:?}, variant_ident: {:?} }},\n",
            it.section, it.anchor, it.english_name, it.variant_ident,
        ));
    }
    out.push_str("];\n");
    Ok(out)
}

/// One information item extracted from the published rec's section
/// hierarchy. Internal to this module; the emitted const carries
/// the same fields via `XML_INFOSET_INFORMATION_ITEMS` (generated into
/// the consumer crate at build time — not a symbol in this crate).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScannedItem {
    pub section: String,
    pub anchor: String,
    pub english_name: String,
    pub variant_ident: String,
}

/// Scan the Infoset rec's XHTML body for `<h3>...<a name="infoitem.*">...</a></h3>`
/// section headings and return one [`ScannedItem`] per match in
/// document order.
///
/// The W3C-published XHTML uses a regular pattern:
///
/// ```html
/// <h3><a name="infoitem.document">2.1. The Document Information Item</a></h3>
/// <h3><a name="infoitem.element">2.2. Element Information Items</a></h3>
/// ```
///
/// We walk each occurrence of `<a name="infoitem.`, extract the
/// anchor suffix, then continue past `">` to the title text up to
/// `</a>`. The leading dotted section number is split off; the
/// remaining "The X Information Item" / "X Information Items"
/// pattern is reduced to its head-noun phrase.
pub fn scan_infoset_items(xhtml: &str) -> Vec<ScannedItem> {
    const ANCHOR_PREFIX: &str = "<a name=\"infoitem.";
    let mut out = Vec::new();
    let mut rest = xhtml;
    while let Some(idx) = rest.find(ANCHOR_PREFIX) {
        let after = &rest[idx + ANCHOR_PREFIX.len()..];
        // Anchor suffix up to the closing `"`.
        let Some(quote_end) = after.find('"') else {
            break;
        };
        let anchor_suffix = &after[..quote_end];
        // Skip the rest of the `<a ...>` opening tag — find the next `>`.
        let Some(gt_end) = after.find('>') else {
            break;
        };
        // Title text from `gt_end + 1` up to `</a>`.
        let body_start = gt_end + 1;
        let body = &after[body_start..];
        let Some(close_idx) = body.find("</a>") else {
            break;
        };
        let title_raw = body[..close_idx].trim();
        // Move `rest` past the close tag.
        rest = &body[close_idx + "</a>".len()..];

        // Parse the title: "2.1. The Document Information Item" /
        // "2.2 Element Information Items" / "2.10 Notation
        // Information Items" — the published rec mixes "X." and "X"
        // for the trailing dot on the section number.
        let (section, title_tail) = match split_section_number(title_raw) {
            Some(p) => p,
            None => continue,
        };
        let english_name = reduce_to_head_noun(&title_tail);
        let variant_ident = make_variant_ident(&english_name);
        let anchor = format!("infoitem.{}", anchor_suffix);
        out.push(ScannedItem {
            section,
            anchor,
            english_name,
            variant_ident,
        });
    }
    out
}

/// Split a section title into (number, rest-of-title), accepting
/// both `"2.1. Foo"` and `"2.1 Foo"` shapes. Returns `None` if the
/// title doesn't start with `<digit>(.<digit>)*[.]? <text>`.
fn split_section_number(s: &str) -> Option<(String, String)> {
    let mut chars = s.char_indices();
    let mut last_digit_or_dot = None;
    for (i, c) in chars.by_ref() {
        if c.is_ascii_digit() || c == '.' {
            last_digit_or_dot = Some(i);
            continue;
        }
        // First non-digit-non-dot character ends the number span.
        last_digit_or_dot?;
        // Number ends at `i`, trim trailing dot if any.
        let end = last_digit_or_dot.unwrap() + 1;
        let raw_num = s[..end].trim_end_matches('.');
        let tail = s[end..].trim_start();
        return Some((raw_num.to_string(), tail.to_string()));
    }
    None
}

/// Reduce "The Document Information Item" → "Document",
/// "Element Information Items" → "Element",
/// "The Document Type Declaration Information Item" → "Document Type Declaration",
/// etc. The "Information Item(s)" suffix is fixed; a leading "The"
/// article is dropped per English noun-phrase grammar (Huddleston &
/// Pullum 2002 §5).
fn reduce_to_head_noun(s: &str) -> String {
    let trimmed = s.trim();
    let without_suffix = trimmed
        .trim_end_matches("Information Items")
        .trim_end_matches("Information Item")
        .trim();
    without_suffix
        .strip_prefix("The ")
        .unwrap_or(without_suffix)
        .trim()
        .to_string()
}

/// Build a Rust-identifier-safe enum-variant name with the
/// `<HeadNoun>Item` shape: "Document" → "DocumentItem",
/// "Document Type Declaration" → "DocumentTypeDeclarationItem",
/// "Unexpanded Entity Reference" → "UnexpandedEntityReferenceItem".
fn make_variant_ident(english_name: &str) -> String {
    let mut out = String::new();
    let mut capitalize_next = true;
    for c in english_name.chars() {
        if c.is_ascii_whitespace() || c == '-' {
            capitalize_next = true;
            continue;
        }
        if capitalize_next {
            out.extend(c.to_uppercase());
            capitalize_next = false;
        } else {
            out.push(c);
        }
    }
    out.push_str("Item");
    out
}

// =============================================================================
// Tests
// =============================================================================

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

    #[crate::praxis_value(Verifiable)]
    #[test]
    fn scan_xsd_attribute_names_finds_lang_space_base_id() {
        let xsd = r#"<?xml version="1.0"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
              <xs:attribute name="lang" />
              <xs:attribute name="space" />
              <xs:attribute name="base" type="xs:anyURI" />
              <xs:attribute name="id" type="xs:ID" />
            </xs:schema>"#;
        let names = scan_xsd_attribute_names(xsd);
        assert_eq!(names, vec!["base", "id", "lang", "space"]);
    }

    #[crate::praxis_value(Verifiable)]
    #[test]
    fn scan_xsd_attribute_names_empty_on_empty_input() {
        assert!(scan_xsd_attribute_names("").is_empty());
    }

    #[crate::praxis_value(Verifiable)]
    #[test]
    fn split_section_number_handles_dot_form() {
        assert_eq!(
            split_section_number("2.1. The Document Information Item"),
            Some((
                "2.1".to_string(),
                "The Document Information Item".to_string()
            ))
        );
    }

    #[crate::praxis_value(Verifiable)]
    #[test]
    fn split_section_number_handles_no_trailing_dot() {
        assert_eq!(
            split_section_number("2.6 Character Information Items"),
            Some(("2.6".to_string(), "Character Information Items".to_string()))
        );
    }

    #[crate::praxis_value(Verifiable)]
    #[test]
    fn reduce_to_head_noun_drops_the_and_suffix() {
        assert_eq!(
            reduce_to_head_noun("The Document Information Item"),
            "Document"
        );
        assert_eq!(reduce_to_head_noun("Element Information Items"), "Element");
        assert_eq!(
            reduce_to_head_noun("The Document Type Declaration Information Item"),
            "Document Type Declaration"
        );
        assert_eq!(
            reduce_to_head_noun("Unexpanded Entity Reference Information Items"),
            "Unexpanded Entity Reference"
        );
    }

    #[crate::praxis_value(Verifiable)]
    #[test]
    fn make_variant_ident_camel_cases_with_item_suffix() {
        assert_eq!(make_variant_ident("Document"), "DocumentItem");
        assert_eq!(
            make_variant_ident("Document Type Declaration"),
            "DocumentTypeDeclarationItem"
        );
        assert_eq!(
            make_variant_ident("Unexpanded Entity Reference"),
            "UnexpandedEntityReferenceItem"
        );
    }

    #[crate::praxis_value(Explainable, Verifiable)]
    #[test]
    fn scan_infoset_items_yields_eleven_in_section_order() {
        // Mirror the W3C-published rec's heading pattern. The actual
        // rec is bundled at crates/domains/data/markup-schemas/xml/.
        let sample = r#"
            <h3><a name="infoitem.document">2.1. The Document Information Item</a></h3>
            <h3><a name="infoitem.element">2.2. Element Information Items</a></h3>
            <h3><a name="infoitem.attribute">2.3. Attribute Information Items</a></h3>
            <h3><a name="infoitem.pi">2.4. Processing Instruction Information Items</a></h3>
            <h3><a name="infoitem.rse">2.5. Unexpanded Entity Reference Information Items</a></h3>
            <h3><a name="infoitem.character">2.6. Character Information Items</a></h3>
            <h3><a name="infoitem.comment">2.7. Comment Information Items</a></h3>
            <h3><a name="infoitem.doctype">2.8. The Document Type Declaration Information Item</a></h3>
            <h3><a name="infoitem.entity.unparsed">2.9. Unparsed Entity Information Items</a></h3>
            <h3><a name="infoitem.notation">2.10. Notation Information Items</a></h3>
            <h3><a name="infoitem.namespace">2.11. Namespace Information Items</a></h3>
        "#;
        let items = scan_infoset_items(sample);
        assert_eq!(items.len(), 11);
        assert_eq!(items[0].english_name, "Document");
        assert_eq!(items[0].variant_ident, "DocumentItem");
        assert_eq!(items[0].section, "2.1");
        assert_eq!(items[10].english_name, "Namespace");
        assert_eq!(items[10].variant_ident, "NamespaceItem");
        // Spot-check the multi-word names.
        let dtd = items
            .iter()
            .find(|i| i.anchor == "infoitem.doctype")
            .unwrap();
        assert_eq!(dtd.english_name, "Document Type Declaration");
        assert_eq!(dtd.variant_ident, "DocumentTypeDeclarationItem");
        let rse = items.iter().find(|i| i.anchor == "infoitem.rse").unwrap();
        assert_eq!(rse.english_name, "Unexpanded Entity Reference");
        assert_eq!(rse.variant_ident, "UnexpandedEntityReferenceItem");
    }
}