diff --git a/Cargo.lock b/Cargo.lock index bf67f5e..54794d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,6 +1178,7 @@ dependencies = [ [[package]] name = "gossamer-rs" version = "0.1.0" +source = "git+https://github.com/metadatastician/gossamer.git?rev=73d8c077616777cdcd99a3c3eda50d5fa8865e2c#73d8c077616777cdcd99a3c3eda50d5fa8865e2c" dependencies = [ "serde", "serde_json", diff --git a/README.adoc b/README.adoc index 833a980..7c15c8b 100644 --- a/README.adoc +++ b/README.adoc @@ -20,17 +20,58 @@ This project must declare **MPL-2.0-or-later** for platform/tooling compatibilit Philosophy: **Palimpsest**. The MPL-2.0 (PMPL) text is provided in `license/MPL-2.0.txt`, and the canonical source is the palimpsest-license repository. -Cross-platform document editor with format tabs (TXT/MD/ADOC/DJOT/ORG/RST/TYP). Gossamer GUI + Ada TUI. Graph visualization, OCR, TTS/STT, Nickel pipelines. +DocMatrix is multi-format document conversion and precision infrastructure for +the wider document suite. The tabbed multi-format viewer/editor is the separate +*Formatrix Docs* project. GUI, TUI, graph, and editor claims in copied or +ecosystem-level documents must not be attributed to DocMatrix itself. + +ForthWall is a proposed capability-bounded Forth execution layer for critical +precision operations. It is not currently implemented or proved. == Features -* *Format Tabs* - View and edit the same document in multiple markup formats -* *Unified AST* - Lossless conversion between formats -* *GUI* - Gossamer with AffineScript frontend -* *TUI* - Ada with AdaCurses for terminal usage -* *Graph Visualization* - ArangoDB for document relationships -* *Accessibility* - OCR, TTS, STT support -* *Pipelines* - Nickel-based import/export transformations +* *Current core* - Parse and render supported formats through a unified AST +* *Current interfaces* - Rust conversion APIs plus a C ABI/FFI surface +* *Suite boundary* - Formatrix Docs owns the tabbed viewer/editor experience +* *Critical-mode proposal* - ForthWall rules bounded to declared inputs, + approved document operations, and independently verified outputs + +== Precision document suite + +DocMatrix is designed to cooperate with two distinct user-facing tools: + +* https://github.com/hyperpolymath/formatrix-docs[Formatrix Docs] — view one + logical document through tabbed TXT, delimiter-selected tabular text, + Markdown, AsciiDoc, Djot, and A2ML representations, with synchronised editing + as the intended progression; and +* https://github.com/hyperpolymath/blocky-writer[Blocky Writer] — fit content + into fixed-layout PDF and application-form boxes, baselines, and + per-character cells that were designed for hand spacing rather than reliable + computer entry. + +The responsibilities must remain separate in code and evidence. Conversion +correctness does not prove synchronised editing, and synchronised editing does +not prove page-coordinate placement. A ForthWall operation may coordinate a +bounded critical step only after the relevant component's own semantic, +round-trip, geometry, confinement, and independent-verification gates pass. +The versioned composition contract is tracked in +https://github.com/hyperpolymath/docmatrix/issues/71[issue #71]; it explicitly +forbids making Microsoft Word or another lossy hub format mandatory. + +== Conversion delivery gate + +DocMatrix conversion claims require independently reproducible tests that +round trips preserve every construct described as lossless, lossy conversions +are reported, input and output formats are identified, and malformed or +ambiguous documents fail without silent rewriting. Viewer/editor behaviours +such as cursor mapping, synchronised tabs, and undo/redo belong to Formatrix +Docs and must be proved there. + +Critical-mode automatic editing through the proposed ForthWall layer has an +additional safety gate: bounded capabilities, exact input hashes and evidence +spans, semantic refusal conditions, replay traces, and independent verification +of every applied edit. See +https://github.com/hyperpolymath/docmatrix/issues/70[issue #70]. == Supported Formats @@ -57,15 +98,22 @@ just deps # Build all components just build -# Run GUI -just run-gui +# Exercise the currently evidenced conversion core +just test-core -# Run TUI -just run-tui +# GUI and TUI recipes are ecosystem scaffolding, not DocMatrix capability proof ---- +`just test-core` covers core parsing, rendering, format identification, round +trips, and no-panic handling. It is not the complete conversion delivery gate: +the suite does not yet provide dedicated loss-reporting or end-to-end ambiguous +format-identification rejection coverage. + == Architecture +The following tree is ecosystem/Formatrix Docs scaffolding retained in this +checkout; it is not the DocMatrix suite boundary: + [source] ---- crates/ diff --git a/crates/formatrix-core/benches/format_bench.rs b/crates/formatrix-core/benches/format_bench.rs index 286dc23..3adf652 100644 --- a/crates/formatrix-core/benches/format_bench.rs +++ b/crates/formatrix-core/benches/format_bench.rs @@ -2,12 +2,12 @@ // Copyright (c) Jonathan D.A. Jewell //! Benchmark tests for format conversion performance +use criterion::{black_box, criterion_group, criterion_main, Criterion}; use formatrix_core::{ ast::{Block, Document, DocumentMeta, Inline, SourceFormat}, - traits::{Parser, ParseConfig, RenderConfig, Renderer}, formats::PlainTextHandler, + traits::{ParseConfig, Parser, RenderConfig, Renderer}, }; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; /// Benchmark plaintext parsing of small documents fn bench_parse_small_plaintext(c: &mut Criterion) { @@ -26,7 +26,8 @@ fn bench_parse_medium_plaintext(c: &mut Criterion) { c.bench_function("parse_medium_plaintext_10kb", |b| { b.iter(|| { let parser = PlainTextHandler::new(); - let input = black_box(&"Lorem ipsum dolor sit amet.\n\n".repeat(500)); + let source = "Lorem ipsum dolor sit amet.\n\n".repeat(500); + let input = black_box(source.as_str()); let config = ParseConfig::default(); parser.parse(input, &config) }) @@ -38,7 +39,8 @@ fn bench_parse_large_plaintext(c: &mut Criterion) { c.bench_function("parse_large_plaintext_100kb", |b| { b.iter(|| { let parser = PlainTextHandler::new(); - let input = black_box(&"Lorem ipsum dolor sit amet.\n\n".repeat(5000)); + let source = "Lorem ipsum dolor sit amet.\n\n".repeat(5000); + let input = black_box(source.as_str()); let config = ParseConfig::default(); parser.parse(input, &config) }) @@ -51,14 +53,12 @@ fn bench_render_plaintext(c: &mut Criterion) { let doc = Document { source_format: SourceFormat::PlainText, meta: DocumentMeta::default(), - content: vec![ - Block::Paragraph { - content: vec![Inline::Text { - content: "Test paragraph content.".to_string(), - }], - span: None, - }, - ], + content: vec![Block::Paragraph { + content: vec![Inline::Text { + content: "Test paragraph content.".to_string(), + }], + span: None, + }], raw_source: None, }; @@ -93,8 +93,10 @@ fn bench_parse_with_source_preservation(c: &mut Criterion) { b.iter(|| { let parser = PlainTextHandler::new(); let input = black_box("Test content with preservation.\n\nAnother paragraph."); - let mut config = ParseConfig::default(); - config.preserve_raw_source = true; + let config = ParseConfig { + preserve_raw_source: true, + ..Default::default() + }; parser.parse(input, &config) }) @@ -107,8 +109,10 @@ fn bench_parse_with_span_preservation(c: &mut Criterion) { b.iter(|| { let parser = PlainTextHandler::new(); let input = black_box("Test content with spans.\n\nAnother paragraph."); - let mut config = ParseConfig::default(); - config.preserve_spans = true; + let config = ParseConfig { + preserve_spans: true, + ..Default::default() + }; parser.parse(input, &config) }) @@ -118,26 +122,22 @@ fn bench_parse_with_span_preservation(c: &mut Criterion) { /// Benchmark document creation fn bench_document_creation(c: &mut Criterion) { c.bench_function("create_document_with_metadata", |b| { - b.iter(|| { - Document { - source_format: SourceFormat::PlainText, - meta: DocumentMeta { - title: Some("Test Title".to_string()), - authors: vec!["Author".to_string()], - date: Some("2026-04-04".to_string()), - language: Some("en".to_string()), - ..Default::default() - }, - content: vec![ - Block::Paragraph { - content: vec![Inline::Text { - content: black_box("Content".to_string()), - }], - span: None, - }, - ], - raw_source: None, - } + b.iter(|| Document { + source_format: SourceFormat::PlainText, + meta: DocumentMeta { + title: Some("Test Title".to_string()), + authors: vec!["Author".to_string()], + date: Some("2026-04-04".to_string()), + language: Some("en".to_string()), + ..Default::default() + }, + content: vec![Block::Paragraph { + content: vec![Inline::Text { + content: black_box("Content".to_string()), + }], + span: None, + }], + raw_source: None, }) }); } @@ -150,7 +150,8 @@ fn bench_batch_parsing(c: &mut Criterion) { let config = ParseConfig::default(); for i in 0..10 { - let input = black_box(&format!("Document {}.\n\nContent.", i)); + let source = format!("Document {}.\n\nContent.", i); + let input = black_box(source.as_str()); let _ = parser.parse(input, &config); } }) diff --git a/crates/formatrix-core/src/ast.rs b/crates/formatrix-core/src/ast.rs index 5b426cd..4c1884d 100644 --- a/crates/formatrix-core/src/ast.rs +++ b/crates/formatrix-core/src/ast.rs @@ -683,16 +683,18 @@ mod proptests { prop_assert!(format.extension() != format.label() || format.extension() == format.label().to_lowercase()); } - // Property: Document word_count is non-negative + // Property: Document word_count composes from its blocks #[test] - fn prop_document_word_count_nonnegative(doc in document_strategy()) { - prop_assert!(doc.word_count() >= 0); + fn prop_document_word_count_composes(doc in document_strategy()) { + let expected: usize = doc.content.iter().map(Block::word_count).sum(); + prop_assert_eq!(doc.word_count(), expected); } - // Property: Document char_count is non-negative + // Property: Document char_count composes from its blocks #[test] - fn prop_document_char_count_nonnegative(doc in document_strategy()) { - prop_assert!(doc.char_count() >= 0); + fn prop_document_char_count_composes(doc in document_strategy()) { + let expected: usize = doc.content.iter().map(Block::char_count).sum(); + prop_assert_eq!(doc.char_count(), expected); } // Property: Empty document has zero word count @@ -742,7 +744,7 @@ mod proptests { span: None, }; if let Block::Heading { level: l, .. } = block { - prop_assert!(l >= 1 && l <= 6); + prop_assert!((1..=6).contains(&l)); } } diff --git a/crates/formatrix-core/src/formats/djot.rs b/crates/formatrix-core/src/formats/djot.rs index bb930e3..6caf763 100644 --- a/crates/formatrix-core/src/formats/djot.rs +++ b/crates/formatrix-core/src/formats/djot.rs @@ -280,7 +280,7 @@ fn container_to_block( Container::Div { class } => { // Check if it's an admonition - let admonition = match class.as_ref() { + let admonition = match class { "note" => Some(AdmonitionType::Note), "tip" => Some(AdmonitionType::Tip), "warning" => Some(AdmonitionType::Warning), @@ -537,11 +537,11 @@ fn render_inline(output: &mut String, inline: &Inline) { } Inline::Strong { content } => { - output.push_str("*"); + output.push('*'); for i in content { render_inline(output, i); } - output.push_str("*"); + output.push('*'); } Inline::Strikethrough { content } => { @@ -602,9 +602,9 @@ fn render_inline(output: &mut String, inline: &Inline) { } Inline::RawInline { content, .. } => { - output.push_str("`"); + output.push('`'); output.push_str(content); - output.push_str("`"); + output.push('`'); } _ => {} diff --git a/crates/formatrix-core/src/formats/markdown.rs b/crates/formatrix-core/src/formats/markdown.rs index 8fc85b6..8ac3b95 100644 --- a/crates/formatrix-core/src/formats/markdown.rs +++ b/crates/formatrix-core/src/formats/markdown.rs @@ -159,29 +159,26 @@ fn parse_node<'a>(node: &'a AstNode<'a>) -> Option { let columns = Vec::new(); // Would need to extract from table alignments for child in node.children() { - match child.data.borrow().value { - NodeValue::TableRow(is_header) => { - let cells: Vec = child - .children() - .map(|cell| TableCell { - content: vec![Block::Paragraph { - content: parse_inlines(cell), - span: None, - }], - colspan: 1, - rowspan: 1, - alignment: None, - }) - .collect(); - - let row = TableRow { cells }; - if is_header { - header = Some(row); - } else { - body.push(row); - } + if let NodeValue::TableRow(is_header) = child.data.borrow().value { + let cells: Vec = child + .children() + .map(|cell| TableCell { + content: vec![Block::Paragraph { + content: parse_inlines(cell), + span: None, + }], + colspan: 1, + rowspan: 1, + alignment: None, + }) + .collect(); + + let row = TableRow { cells }; + if is_header { + header = Some(row); + } else { + body.push(row); } - _ => {} } } diff --git a/crates/formatrix-core/src/formats/orgmode.rs b/crates/formatrix-core/src/formats/orgmode.rs index 891d81f..862bd20 100644 --- a/crates/formatrix-core/src/formats/orgmode.rs +++ b/crates/formatrix-core/src/formats/orgmode.rs @@ -276,7 +276,7 @@ where use orgize::Event; let mut inlines = Vec::new(); - while let Some(event) = events.next() { + for event in events.by_ref() { match &event { Event::End(elem) if is_end_element(elem) => break, Event::Start(Element::Text { value }) | Event::End(Element::Text { value }) => { @@ -480,7 +480,7 @@ where I: Iterator>, { use orgize::Event; - while let Some(event) = events.next() { + for event in events.by_ref() { if matches!(event, Event::End(Element::TableRow(_))) { break; } @@ -531,11 +531,11 @@ where use orgize::Event; let mut text = String::new(); - while let Some(event) = events.next() { + for event in events.by_ref() { match event { Event::End(Element::TableCell(_)) => break, Event::Start(Element::Text { value }) | Event::End(Element::Text { value }) => { - text.push_str(&value); + text.push_str(value); } _ => {} } diff --git a/crates/formatrix-core/src/formats/rst.rs b/crates/formatrix-core/src/formats/rst.rs index 33b1a5b..b984d8b 100644 --- a/crates/formatrix-core/src/formats/rst.rs +++ b/crates/formatrix-core/src/formats/rst.rs @@ -142,13 +142,10 @@ fn convert_body_element(element: &BodyElement) -> Option { BodyElement::BlockQuote(bq) => { let mut inner_blocks = Vec::new(); for child in bq.children() { - match child { - document_tree::element_categories::SubBlockQuote::BodyElement(be) => { - if let Some(block) = convert_body_element(be) { - inner_blocks.push(block); - } + if let document_tree::element_categories::SubBlockQuote::BodyElement(be) = child { + if let Some(block) = convert_body_element(be) { + inner_blocks.push(block); } - _ => {} } } Some(Block::BlockQuote { @@ -163,18 +160,18 @@ fn convert_body_element(element: &BodyElement) -> Option { let items: Vec = bl .children() .iter() - .filter_map(|item| { + .map(|item| { let item_blocks: Vec = item .children() .iter() - .filter_map(|child| convert_body_element(child)) + .filter_map(convert_body_element) .collect(); - Some(ListItem { + ListItem { content: item_blocks, checked: None, marker: None, - }) + } }) .collect(); @@ -190,18 +187,18 @@ fn convert_body_element(element: &BodyElement) -> Option { let items: Vec = el .children() .iter() - .filter_map(|item| { + .map(|item| { let item_blocks: Vec = item .children() .iter() - .filter_map(|child| convert_body_element(child)) + .filter_map(convert_body_element) .collect(); - Some(ListItem { + ListItem { content: item_blocks, checked: None, marker: None, - }) + } }) .collect(); @@ -434,7 +431,7 @@ fn render_block(output: &mut String, block: &Block, _depth: usize) { 4 => '^', _ => '\'', }; - let len = content.iter().map(|i| inline_text_len(i)).sum::(); + let len = content.iter().map(inline_text_len).sum::(); output.push_str(&underline.to_string().repeat(len.max(1))); } @@ -583,7 +580,7 @@ fn render_inline(output: &mut String, inline: &Inline) { } Inline::LineBreak => { - output.push_str("\n"); + output.push('\n'); } Inline::SoftBreak => { diff --git a/crates/formatrix-core/src/formats/typst.rs b/crates/formatrix-core/src/formats/typst.rs index 54dce31..b25c94f 100644 --- a/crates/formatrix-core/src/formats/typst.rs +++ b/crates/formatrix-core/src/formats/typst.rs @@ -88,14 +88,14 @@ fn parse_syntax_tree(root: &SyntaxNode) -> Vec { } // Parse heading - if let Some(heading) = parse_heading(&child) { + if let Some(heading) = parse_heading(child) { blocks.push(heading); } } SyntaxKind::ListItem => { // Handle list items - if let Some(item) = parse_list_item(&child) { + if let Some(item) = parse_list_item(child) { // Check if we can append to existing list if let Some(Block::List { items, .. }) = blocks.last_mut() { items.push(item); @@ -111,7 +111,7 @@ fn parse_syntax_tree(root: &SyntaxNode) -> Vec { } SyntaxKind::EnumItem => { - if let Some(item) = parse_list_item(&child) { + if let Some(item) = parse_list_item(child) { if let Some(Block::List { kind: ListKind::Ordered, items, @@ -132,8 +132,8 @@ fn parse_syntax_tree(root: &SyntaxNode) -> Vec { SyntaxKind::Raw => { // Code block - let content = extract_raw_content(&child); - let language = extract_raw_language(&child); + let content = extract_raw_content(child); + let language = extract_raw_language(child); blocks.push(Block::CodeBlock { language, content, @@ -156,21 +156,21 @@ fn parse_syntax_tree(root: &SyntaxNode) -> Vec { } SyntaxKind::Strong => { - current_text.push_str(&format!("*{}*", extract_text(&child))); + current_text.push_str(&format!("*{}*", extract_text(child))); } SyntaxKind::Emph => { - current_text.push_str(&format!("_{}_", extract_text(&child))); + current_text.push_str(&format!("_{}_", extract_text(child))); } SyntaxKind::Link => { - let url = extract_text(&child); + let url = extract_text(child); current_text.push_str(&url); } SyntaxKind::Markup => { // Recurse into markup content - let inner_blocks = parse_syntax_tree(&child); + let inner_blocks = parse_syntax_tree(child); blocks.extend(inner_blocks); } diff --git a/crates/formatrix-core/src/traits.rs b/crates/formatrix-core/src/traits.rs index 0dda0c7..5d268d4 100644 --- a/crates/formatrix-core/src/traits.rs +++ b/crates/formatrix-core/src/traits.rs @@ -155,10 +155,6 @@ impl FormatRegistry { parse_config: &ParseConfig, render_config: &RenderConfig, ) -> Result { - if from == to { - return Ok(input.to_string()); - } - let from_handler = self .get(from) .ok_or_else(|| ConversionError::UnsupportedFeature { @@ -166,6 +162,11 @@ impl FormatRegistry { feature: "parsing".to_string(), })?; + if from == to { + from_handler.parse(input, parse_config)?; + return Ok(input.to_string()); + } + let to_handler = self .get(to) .ok_or_else(|| ConversionError::UnsupportedFeature { @@ -183,3 +184,82 @@ impl Default for FormatRegistry { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + + struct ValidatingHandler; + + impl Parser for ValidatingHandler { + fn format(&self) -> SourceFormat { + SourceFormat::PlainText + } + + fn parse(&self, input: &str, _config: &ParseConfig) -> Result { + if input == "ambiguous" { + return Err(ConversionError::ParseError { + line: 1, + column: 1, + message: "ambiguous test document".to_string(), + }); + } + + Ok(Document::new(SourceFormat::PlainText)) + } + } + + impl Renderer for ValidatingHandler { + fn format(&self) -> SourceFormat { + SourceFormat::PlainText + } + + fn render(&self, _doc: &Document, _config: &RenderConfig) -> Result { + unreachable!("identity conversion must not render") + } + } + + impl FormatHandler for ValidatingHandler { + fn supports_feature(&self, _feature: &str) -> bool { + false + } + + fn supported_features(&self) -> &[&str] { + &[] + } + } + + #[test] + fn identity_conversion_validates_and_preserves_input() { + let mut registry = FormatRegistry::new(); + registry.register(Box::new(ValidatingHandler)); + + let result = registry + .convert( + "valid input\n", + SourceFormat::PlainText, + SourceFormat::PlainText, + &ParseConfig::default(), + &RenderConfig::default(), + ) + .unwrap(); + + assert_eq!(result, "valid input\n"); + } + + #[test] + fn identity_conversion_rejects_parser_errors() { + let mut registry = FormatRegistry::new(); + registry.register(Box::new(ValidatingHandler)); + + let result = registry.convert( + "ambiguous", + SourceFormat::PlainText, + SourceFormat::PlainText, + &ParseConfig::default(), + &RenderConfig::default(), + ); + + assert!(matches!(result, Err(ConversionError::ParseError { .. }))); + } +} diff --git a/crates/formatrix-core/tests/aspect_test.rs b/crates/formatrix-core/tests/aspect_test.rs index d530291..e5f7a25 100644 --- a/crates/formatrix-core/tests/aspect_test.rs +++ b/crates/formatrix-core/tests/aspect_test.rs @@ -3,9 +3,9 @@ //! Aspect and cross-cutting concern tests use formatrix_core::{ - ast::{Document, DocumentMeta, SourceFormat}, - traits::{Parser, ParseConfig}, + ast::{DocumentMeta, SourceFormat}, formats::PlainTextHandler, + traits::{ParseConfig, Parser}, }; /// Test handling of oversized documents (1MB+) @@ -18,7 +18,10 @@ fn test_large_document_handling() { let config = ParseConfig::default(); let result = parser.parse(&large_content, &config); - assert!(result.is_ok(), "should handle large documents without panic"); + assert!( + result.is_ok(), + "should handle large documents without panic" + ); } /// Test handling of extremely deep nesting @@ -34,7 +37,10 @@ fn test_deeply_nested_elements() { let config = ParseConfig::default(); let result = parser.parse(&nested, &config); - assert!(result.is_ok(), "should handle deep nesting without stack overflow"); + assert!( + result.is_ok(), + "should handle deep nesting without stack overflow" + ); } /// Test handling of null bytes (safety aspect) @@ -73,7 +79,10 @@ fn test_unicode_cjk_preservation() { let config = ParseConfig::default(); let doc = parser.parse(input, &config).expect("parse failed"); - assert!(!doc.content.is_empty(), "CJK text should parse successfully"); + assert!( + !doc.content.is_empty(), + "CJK text should parse successfully" + ); } /// Test Unicode emoji preservation @@ -97,7 +106,10 @@ fn test_unicode_rtl_preservation() { let config = ParseConfig::default(); let doc = parser.parse(input, &config).expect("parse failed"); - assert!(!doc.content.is_empty(), "RTL text should parse successfully"); + assert!( + !doc.content.is_empty(), + "RTL text should parse successfully" + ); } /// Test zero-width characters @@ -122,7 +134,11 @@ fn test_empty_document_no_panic() { let config = ParseConfig::default(); let doc = parser.parse(input, &config).expect("parse failed"); - assert_eq!(doc.content.len(), 0, "empty input should produce empty content"); + assert_eq!( + doc.content.len(), + 0, + "empty input should produce empty content" + ); } /// Test single whitespace character @@ -171,7 +187,10 @@ fn test_consecutive_blank_lines() { let doc = parser.parse(input, &config).expect("parse failed"); // Should handle multiple blank lines gracefully - assert!(doc.content.len() <= 2, "consecutive blanks should not create extra blocks"); + assert!( + doc.content.len() <= 2, + "consecutive blanks should not create extra blocks" + ); } /// Test document with only punctuation @@ -216,9 +235,7 @@ fn test_document_metadata_large_values() { let large_title = "A".repeat(100_000); let meta = DocumentMeta { title: Some(large_title.clone()), - authors: (0..1000) - .map(|i| format!("Author {}", i)) - .collect(), + authors: (0..1000).map(|i| format!("Author {}", i)).collect(), ..Default::default() }; @@ -233,10 +250,9 @@ fn test_parse_config_large_options() { // Add many options for i in 0..1000 { - config.format_options.insert( - format!("option_{}", i), - format!("value_{}", i), - ); + config + .format_options + .insert(format!("option_{}", i), format!("value_{}", i)); } assert_eq!(config.format_options.len(), 1000); diff --git a/crates/formatrix-core/tests/e2e_test.rs b/crates/formatrix-core/tests/e2e_test.rs index e0fbfd8..21bbd7e 100644 --- a/crates/formatrix-core/tests/e2e_test.rs +++ b/crates/formatrix-core/tests/e2e_test.rs @@ -4,8 +4,8 @@ use formatrix_core::{ ast::{Block, Document, DocumentMeta, Inline, SourceFormat}, - traits::{Parser, ParseConfig, RenderConfig, Renderer}, formats::PlainTextHandler, + traits::{ParseConfig, Parser, RenderConfig, Renderer}, }; /// Test basic plaintext parsing @@ -49,8 +49,10 @@ fn test_plaintext_parse_multiple_paragraphs() { fn test_plaintext_preserve_raw_source() { let parser = PlainTextHandler::new(); let input = "Test content with\nmultiple lines."; - let mut config = ParseConfig::default(); - config.preserve_raw_source = true; + let config = ParseConfig { + preserve_raw_source: true, + ..Default::default() + }; let doc = parser.parse(input, &config).expect("parse failed"); assert!(doc.raw_source.is_some(), "raw_source should be preserved"); @@ -60,7 +62,6 @@ fn test_plaintext_preserve_raw_source() { /// Test plaintext render from AST #[test] fn test_plaintext_render_document() { - let parser = PlainTextHandler::new(); let renderer = PlainTextHandler::new(); let doc = Document { @@ -69,14 +70,12 @@ fn test_plaintext_render_document() { title: Some("Test Document".to_string()), ..Default::default() }, - content: vec![ - Block::Paragraph { - content: vec![Inline::Text { - content: "Hello, world!".to_string(), - }], - span: None, - }, - ], + content: vec![Block::Paragraph { + content: vec![Inline::Text { + content: "Hello, world!".to_string(), + }], + span: None, + }], raw_source: None, }; @@ -93,7 +92,11 @@ fn test_plaintext_empty_document() { let config = ParseConfig::default(); let doc = parser.parse(input, &config).expect("parse failed"); - assert_eq!(doc.content.len(), 0, "empty input should produce empty content"); + assert_eq!( + doc.content.len(), + 0, + "empty input should produce empty content" + ); } /// Test document with only whitespace @@ -104,7 +107,11 @@ fn test_plaintext_whitespace_only() { let config = ParseConfig::default(); let doc = parser.parse(input, &config).expect("parse failed"); - assert_eq!(doc.content.len(), 0, "whitespace-only input should produce no blocks"); + assert_eq!( + doc.content.len(), + 0, + "whitespace-only input should produce no blocks" + ); } /// Test round-trip: parse then render @@ -121,14 +128,22 @@ fn test_plaintext_round_trip() { let doc = parser.parse(input, &parse_config).expect("parse failed"); // Render - let output = renderer.render(&doc, &render_config).expect("render failed"); + let output = renderer + .render(&doc, &render_config) + .expect("render failed"); // Both should be valid (content-wise equivalent) assert!(!output.is_empty(), "round-trip output should not be empty"); // Re-parse the output - let doc2 = parser.parse(&output, &parse_config).expect("re-parse failed"); - assert_eq!(doc.content.len(), doc2.content.len(), "round-trip block count mismatch"); + let doc2 = parser + .parse(&output, &parse_config) + .expect("re-parse failed"); + assert_eq!( + doc.content.len(), + doc2.content.len(), + "round-trip block count mismatch" + ); } /// Test parser format identification @@ -210,14 +225,12 @@ fn test_document_clone() { title: Some("Original".to_string()), ..Default::default() }, - content: vec![ - Block::Paragraph { - content: vec![Inline::Text { - content: "Content".to_string(), - }], - span: None, - }, - ], + content: vec![Block::Paragraph { + content: vec![Inline::Text { + content: "Content".to_string(), + }], + span: None, + }], raw_source: Some("Raw".to_string()), }; @@ -240,10 +253,12 @@ fn test_parse_config_immutability() { /// Test render config customization #[test] fn test_render_config_customization() { - let mut config = RenderConfig::default(); - config.line_width = 120; - config.indent = "\t".to_string(); - config.hard_breaks = true; + let config = RenderConfig { + line_width: 120, + indent: "\t".to_string(), + hard_breaks: true, + ..Default::default() + }; assert_eq!(config.line_width, 120); assert_eq!(config.indent, "\t"); diff --git a/crates/formatrix-core/tests/property_test.rs b/crates/formatrix-core/tests/property_test.rs index 819fa38..9ad36c8 100644 --- a/crates/formatrix-core/tests/property_test.rs +++ b/crates/formatrix-core/tests/property_test.rs @@ -4,8 +4,8 @@ use formatrix_core::{ ast::{DocumentMeta, SourceFormat}, - traits::{Parser, ParseConfig, RenderConfig, Renderer}, formats::PlainTextHandler, + traits::{ParseConfig, Parser, RenderConfig, Renderer}, }; use proptest::prelude::*; @@ -37,10 +37,10 @@ proptest! { ) { let config = RenderConfig::default(); - // Line width should be positive or zero - prop_assert!(config.line_width >= 0); - // Indent should exist - prop_assert!(!config.indent.is_empty()); + prop_assert_eq!(config.line_width, 80); + prop_assert_eq!(config.indent, " "); + prop_assert!(!config.hard_breaks); + prop_assert!(config.format_options.is_empty()); } /// Property: Parse config format options preserve insertion order @@ -85,7 +85,7 @@ proptest! { let doc2 = parser.parse(&output, &parse_config).expect("reparse"); // Should have non-empty blocks - prop_assert!(doc2.content.len() > 0); + prop_assert!(!doc2.content.is_empty()); } } diff --git a/crates/formatrix-core/tests/unit_test.rs b/crates/formatrix-core/tests/unit_test.rs index 818ad49..6a9d1c9 100644 --- a/crates/formatrix-core/tests/unit_test.rs +++ b/crates/formatrix-core/tests/unit_test.rs @@ -3,9 +3,9 @@ //! Comprehensive unit tests for formatrix-core use formatrix_core::{ - ast::{Block, Document, DocumentMeta, Inline, SourceFormat, MetaValue}, - traits::{FormatHandler, Parser, ParseConfig, RenderConfig, Renderer}, + ast::{Block, Document, DocumentMeta, Inline, MetaValue, SourceFormat}, formats::PlainTextHandler, + traits::{FormatHandler, ParseConfig, Parser, RenderConfig, Renderer}, }; use std::collections::HashMap; @@ -38,8 +38,10 @@ fn test_parser_multiple_paragraphs() { fn test_parser_preserves_raw_source() { let parser = PlainTextHandler::new(); let input = "Test input"; - let mut config = ParseConfig::default(); - config.preserve_raw_source = true; + let config = ParseConfig { + preserve_raw_source: true, + ..Default::default() + }; let doc = parser.parse(input, &config).expect("parse failed"); assert!(doc.raw_source.is_some()); @@ -91,7 +93,9 @@ fn test_renderer_single_paragraph() { raw_source: None, }; - let output = renderer.render(&doc, &RenderConfig::default()).expect("render failed"); + let output = renderer + .render(&doc, &RenderConfig::default()) + .expect("render failed"); assert_eq!(output, "Hello world"); } @@ -118,7 +122,9 @@ fn test_renderer_multiple_paragraphs() { raw_source: None, }; - let output = renderer.render(&doc, &RenderConfig::default()).expect("render failed"); + let output = renderer + .render(&doc, &RenderConfig::default()) + .expect("render failed"); assert_eq!(output, "Para 1\n\nPara 2"); } @@ -132,7 +138,9 @@ fn test_renderer_empty_document() { raw_source: None, }; - let output = renderer.render(&doc, &RenderConfig::default()).expect("render failed"); + let output = renderer + .render(&doc, &RenderConfig::default()) + .expect("render failed"); assert_eq!(output, ""); } @@ -153,7 +161,9 @@ fn test_renderer_heading() { raw_source: None, }; - let output = renderer.render(&doc, &RenderConfig::default()).expect("render failed"); + let output = renderer + .render(&doc, &RenderConfig::default()) + .expect("render failed"); assert_eq!(output, "Title"); } @@ -177,7 +187,9 @@ fn test_roundtrip_simple() { let render_config = RenderConfig::default(); let doc = parser.parse(input, &config).expect("parse failed"); - let output = renderer.render(&doc, &render_config).expect("render failed"); + let output = renderer + .render(&doc, &render_config) + .expect("render failed"); assert_eq!(output, input); } @@ -192,7 +204,9 @@ fn test_roundtrip_multiple_paragraphs() { let render_config = RenderConfig::default(); let doc = parser.parse(input, &config).expect("parse failed"); - let output = renderer.render(&doc, &render_config).expect("render failed"); + let output = renderer + .render(&doc, &render_config) + .expect("render failed"); assert_eq!(output, input); } @@ -340,11 +354,15 @@ fn test_parse_config_default() { #[test] fn test_parse_config_customization() { - let mut config = ParseConfig::default(); - config.preserve_spans = true; - config.preserve_raw_source = true; - config.front_matter_delimiter = Some("---".to_string()); - config.format_options.insert("key".to_string(), "value".to_string()); + let mut config = ParseConfig { + preserve_spans: true, + preserve_raw_source: true, + front_matter_delimiter: Some("---".to_string()), + ..Default::default() + }; + config + .format_options + .insert("key".to_string(), "value".to_string()); assert!(config.preserve_spans); assert!(config.preserve_raw_source); @@ -363,11 +381,15 @@ fn test_render_config_default() { #[test] fn test_render_config_customization() { - let mut config = RenderConfig::default(); - config.line_width = 120; - config.indent = "\t".to_string(); - config.hard_breaks = true; - config.format_options.insert("opt".to_string(), "val".to_string()); + let mut config = RenderConfig { + line_width: 120, + indent: "\t".to_string(), + hard_breaks: true, + ..Default::default() + }; + config + .format_options + .insert("opt".to_string(), "val".to_string()); assert_eq!(config.line_width, 120); assert_eq!(config.indent, "\t"); @@ -420,9 +442,9 @@ fn test_metavalue_integer() { #[test] fn test_metavalue_float() { - let val = MetaValue::Float(3.14); + let val = MetaValue::Float(1.25); match val { - MetaValue::Float(f) => assert!((f - 3.14).abs() < 0.01), + MetaValue::Float(f) => assert!((f - 1.25).abs() < f64::EPSILON), _ => panic!("expected float"), } } @@ -447,14 +469,7 @@ fn test_metavalue_list() { fn test_parse_never_panics_on_input() { let parser = PlainTextHandler::new(); let large_str = "A".repeat(100_000); - let malformed_inputs: Vec<&str> = vec![ - "", - " ", - "\n", - "\0", - &large_str, - "\u{FFFD}", - ]; + let malformed_inputs: Vec<&str> = vec!["", " ", "\n", "\0", &large_str, "\u{FFFD}"]; for input in malformed_inputs { let result = parser.parse(input, &ParseConfig::default()); diff --git a/crates/formatrix-gui/Cargo.toml b/crates/formatrix-gui/Cargo.toml index b136cf4..68b394f 100644 --- a/crates/formatrix-gui/Cargo.toml +++ b/crates/formatrix-gui/Cargo.toml @@ -19,7 +19,10 @@ path = "src/main.rs" [dependencies] formatrix-core = { path = "../formatrix-core" } -gossamer-rs = { path = "../../../gossamer/bindings/rust" } +# A sibling checkout exists in the developer estate but not on CI runners or in +# published source archives. Pin the actual upstream repository so this +# workspace is self-resolving and reproducible outside that local layout. +gossamer-rs = { git = "https://github.com/metadatastician/gossamer.git", rev = "73d8c077616777cdcd99a3c3eda50d5fa8865e2c" } serde.workspace = true serde_json.workspace = true diff --git a/crates/formatrix-gui/src/commands.rs b/crates/formatrix-gui/src/commands.rs index a8d5599..02ae9ec 100644 --- a/crates/formatrix-gui/src/commands.rs +++ b/crates/formatrix-gui/src/commands.rs @@ -178,8 +178,8 @@ pub struct ConversionResult { /// Load a document from the filesystem (synchronous — uses std::fs) pub fn load_document(path: String) -> Result { - let content = std::fs::read_to_string(&path) - .map_err(|e| format!("Failed to read file: {}", e))?; + let content = + std::fs::read_to_string(&path).map_err(|e| format!("Failed to read file: {}", e))?; // Detect format from extension let format = std::path::Path::new(&path) @@ -219,8 +219,7 @@ pub fn save_document( content: String, format: String, ) -> Result { - std::fs::write(&path, &content) - .map_err(|e| format!("Failed to write file: {}", e))?; + std::fs::write(&path, &content).map_err(|e| format!("Failed to write file: {}", e))?; let word_count = content.split_whitespace().count(); let char_count = content.chars().count(); @@ -246,14 +245,6 @@ pub fn convert_to_format( }; use formatrix_core::traits::{Parser, Renderer}; - // For now, just return the content as-is if converting to same format - if from_format == to_format { - return Ok(ConversionResult { - content, - warnings: Vec::new(), - }); - } - // Parse source format let parse_config = ParseConfig::default(); let render_config = RenderConfig::default(); @@ -285,6 +276,14 @@ pub fn convert_to_format( } }; + // Preserve identity conversions exactly, but only after validating input. + if from_format == to_format { + return Ok(ConversionResult { + content, + warnings: Vec::new(), + }); + } + // Render to target format let output = match to_format.as_str() { "txt" => PlainTextHandler::new() @@ -481,3 +480,28 @@ pub fn get_supported_formats() -> Vec { }, ] } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identity_conversion_validates_and_preserves_input() { + let input = "Heading\n=======\n".to_string(); + + let result = convert_to_format(input.clone(), "rst".to_string(), "rst".to_string()) + .expect("valid RST should pass identity validation"); + + assert_eq!(result.content, input); + assert!(result.warnings.is_empty()); + } + + #[test] + fn identity_conversion_rejects_malformed_input() { + let malformed = ".. image:: example.png\n :scale: not-a-number\n".to_string(); + + let result = convert_to_format(malformed, "rst".to_string(), "rst".to_string()); + + assert!(result.is_err()); + } +} diff --git a/docs/DOCUMENTATION-SYSTEM-ARCHITECTURE.adoc b/docs/DOCUMENTATION-SYSTEM-ARCHITECTURE.adoc index a433839..687eb55 100644 --- a/docs/DOCUMENTATION-SYSTEM-ARCHITECTURE.adoc +++ b/docs/DOCUMENTATION-SYSTEM-ARCHITECTURE.adoc @@ -10,6 +10,12 @@ The Hyperpolymath Documentation System consists of four integrated components that handle document creation, reconciliation, enforcement, and distribution. +WARNING: This document describes a target architecture, not the currently +delivered system. Boxes, source trees, examples, and arrows are proposals unless +linked to executable code and independently reproducible evidence. The current +recon-silly-ation state records both its reconciliation engine and proposed +ForthWall VM as 0% complete. + [plantuml,components,svg] ---- @startuml @@ -17,12 +23,12 @@ skinparam componentStyle rectangle package "Human Side" { [Formatrix Docs] as FD - note right of FD : GUI (Tauri)\nTUI (Ada)\nMulti-format editor + note right of FD : Multi-format viewer\nGUI/TUI editor target\nSynchronised views } package "Machine Side" { [Recon-Silly-Ation] as RSA - note right of RSA : ReconForth DSL\nReconciliation engine\nConflict resolution + note right of RSA : Proposed ForthWall DSL\nConsistency reconciler\nConflict reporting [Docubot] as DB note right of DB : LLM integration\nDoc generation\nGuardrails @@ -51,28 +57,34 @@ PS --> ARANGO : Pack manifests == Component Breakdown -=== 1. Formatrix Docs (Human Editor) +=== 1. Formatrix Docs (Multi-Format Viewer; Editor Target) *Location*: `~/repos/formatrix-docs` -*Purpose*: Cross-platform document editor with format tabs +*Conversion dependency*: DocMatrix supplies the independently tested parsers, +renderers, format identification, and unified AST conversion core. -*Capabilities*: +*Proposed application purpose*: Cross-platform multi-format viewer/editor with +synchronised format tabs. Editor behaviour is not a current DocMatrix +capability. -* View/edit same document in 7 formats (TXT, MD, ADOC, DJOT, ORG, RST, TYP) -* Live format conversion via unified AST -* Graph visualization of document links -* OCR, TTS/STT, spell checking -* Git integration -* Nickel-based content pipelines +*Current and proposed capabilities*: + +* Current dependency: DocMatrix parses and renders supported formats through a + unified AST +* Proposed Formatrix Docs: view/edit the same document in 7 synchronised format + views +* Proposed: graph visualisation, OCR, TTS/STT, spell checking, Git integration, + and Nickel-based content pipelines *Architecture*: [source] ---- +DocMatrix (external conversion core) ──> Formatrix Docs + formatrix-docs/ ├── crates/ -│ ├── formatrix-core/ # Unified AST + converters (Rust) │ ├── formatrix-gui/ # Tauri 2.0 commands │ ├── formatrix-db/ # ArangoDB client │ └── formatrix-pipeline/ # Nickel executor @@ -105,24 +117,33 @@ pub struct Document { *Location*: `~/repos/recon-silly-ation` -*Purpose*: Reconcile, deduplicate, and validate documentation bundles +*Purpose*: Find and safely reconcile contradictions across documentation, +history, metadata, terminology, language policy, attribution, and repository +surfaces -*Capabilities*: +*Proposed capabilities (not currently delivered)*: -* ReconForth DSL for reconciliation rules +* ForthWall DSL for bounded reconciliation rules * 7-stage idempotent pipeline (Scan → Normalize → Deduplicate → Detect → Resolve → Ingest → Report) * Content-addressable storage (SHA-256) * Graph-based conflict resolution * Enforcement bot for automated policy compliance * Pack shipper for bundle distribution -*Architecture*: +ForthWall is an optional implementation layer beneath recon-silly-ation, not +the meaning of “recon” or the identity of the product. Its permitted vocabulary +must operate only on declared documents and evidence. Raw memory, unrestricted +filesystem writes, shell execution, and networking are outside the wall. +Ambiguous semantics, stale inputs, parser disagreement, or coordinate drift +must cause refusal rather than automatic repair. + +*Proposed architecture*: [source] ---- recon-silly-ation/ ├── src/ -│ ├── ReconForth.res # AffineScript bindings to WASM +│ ├── ForthWall.res # proposed AffineScript bindings to bounded WASM │ ├── EnforcementBot.res # Policy enforcement │ ├── PackShipper.res # Bundle distribution │ ├── LogicEngine.res # Datalog-style inference @@ -130,7 +151,7 @@ recon-silly-ation/ ├── wasm-modules/ │ └── src/ │ ├── lib.rs # WASM exports -│ └── reconforth/ # Forth interpreter +│ └── forthwall/ # proposed capability-bounded Forth interpreter │ ├── lexer.rs │ ├── types.rs │ ├── vm.rs @@ -139,7 +160,7 @@ recon-silly-ation/ └── validator/ # Haskell schema validation ---- -*ReconForth Example*: +*Illustrative ForthWall syntax (not executable evidence)*: [source,forth] ---- @@ -254,8 +275,8 @@ Docubot ┌──────────────────────┐ ┌──────────────────────────────────────┐ │ DOCUBOT │ │ RECON-SILLY-ATION │ │ ┌────────────────┐ │ │ ┌──────────────┐ ┌──────────────┐ │ -│ │ LLM Engine │ │ │ │ ReconForth │ │ LogicEngine │ │ -│ │ (guardrailed) │ │ │ │ VM │ │ (Datalog) │ │ +│ │ LLM Engine │ │ │ │ ForthWall │ │ LogicEngine │ │ +│ │ (guardrailed) │ │ │ │ (proposed) │ │ (Datalog) │ │ │ └────────┬───────┘ │ │ └──────┬───────┘ └──────┬───────┘ │ └───────────┼──────────┘ │ └─────────────────┘ │ │ │ │ │ @@ -431,14 +452,26 @@ services: === Adding a New Document Type -1. *formatrix-docs*: Add parser and renderer to formatrix-core -2. *recon-silly-ation*: Add format detection in ReconForth -3. *Docubot*: Update templates for the new format -4. *Docudactyl*: Add format to pipeline configuration +1. *DocMatrix conversion core (required)*: implement and independently prove + the parser, renderer, format identification, and declared round-trip/loss + behaviour. +2. *Optional reconciliation composition*: only when reconciliation is explicitly + enabled, add ForthWall detection after its capability-confinement, semantic + refusal, replay, and independent-proof gates pass. +3. *Optional Docubot integration*: update templates only when generation for the + new format is enabled. +4. *Optional Docudactyl integration*: add the format only to pipelines that + explicitly enable it. + +Format conversion, reconciliation detection, and automatic repair are separate +capabilities and must be composed explicitly. Detection alone does not enable +repair: automatic repair requires its own implementation evidence plus proofs +of bounded edits, exact input/evidence binding, refusal on ambiguity or drift, +replayability, and independent output verification. === Adding a New Enforcement Rule -1. Define rule in ReconForth: +1. Define a bounded rule in ForthWall: + [source,forth] ---- @@ -452,7 +485,7 @@ services: === Adding a New Pack Specification -1. Define pack in ReconForth: +1. Define pack in ForthWall: + [source,forth] ---- @@ -489,10 +522,13 @@ services: |Component |Role |*Formatrix Docs* -|Human-facing editor with format tabs and graph view +|Multi-format viewer and proposed human-facing editor that depends on DocMatrix +for conversion; format tabs, editing, and graph view remain delivery-gated +targets |*Recon-Silly-Ation* -|Machine reconciliation with ReconForth DSL +|Cross-document consistency reconciliation with an optional, proposed +capability-bounded ForthWall DSL |*Docubot* |LLM-powered generation with mandatory guardrails @@ -501,7 +537,10 @@ services: |Orchestrator connecting all components |=== -The four components form a complete documentation lifecycle: +The four proposed components are intended to cover the following logical-document +lifecycle once their implementation and evidence gates are satisfied. Fixed-layout +form filling and coordinate-aware authoring remain the responsibility of Blocky +Writer: ---- Edit (formatrix) → Reconcile (RSA) → Enforce (bot) → Ship (packs) diff --git a/docs/MOSCOW-REQUIREMENTS.adoc b/docs/MOSCOW-REQUIREMENTS.adoc index b1db376..cdff45b 100644 --- a/docs/MOSCOW-REQUIREMENTS.adoc +++ b/docs/MOSCOW-REQUIREMENTS.adoc @@ -8,9 +8,18 @@ == Overview -This document defines Must/Should/Could requirements for each component of the documentation ecosystem, organized for systematic implementation with integration seam checks. +This document defines Must/Should/Could requirements for each component of the +documentation ecosystem, organized for systematic implementation with +integration seam checks. “Done” identifies a current implementation status +recorded by this requirements source; readiness still requires independently +reproducible evidence. Viewer/editor targets still marked pending are proposed +capabilities and are not implied by the component heading. -== Component 1: Formatrix Docs (Human Editor) +== Component 1: Formatrix Docs (Multi-Format Viewer; Proposed Editor) + +DocMatrix provides the current conversion core. Formatrix Docs viewer/editor +requirements are current only where their individual status is `Done`; pending +format-tab and editor requirements describe the proposed application. === MUST Have @@ -194,68 +203,68 @@ This document defines Must/Should/Could requirements for each component of the d |ID |Requirement |Status |RSA-M01 -|ReconForth lexer (tokenization) -|✅ Done +|ForthWall lexer (tokenization) +|🔲 Pending |RSA-M02 -|ReconForth VM (stack, dictionary) -|✅ Done +|ForthWall VM (stack, dictionary) +|🔲 Pending |RSA-M03 |Stack manipulation words (dup, drop, swap, over, rot) -|✅ Done +|🔲 Pending |RSA-M04 |Arithmetic words (+, -, *, /, mod) -|✅ Done +|🔲 Pending |RSA-M05 |Comparison words (=, <, >, <=, >=, <>) -|✅ Done +|🔲 Pending |RSA-M06 |Logic words (and, or, not) -|✅ Done +|🔲 Pending |RSA-M07 |Control flow words (if/else/then, call) -|✅ Done +|🔲 Pending |RSA-M08 |Document words (new-doc, doc-hash, doc-content) -|✅ Done +|🔲 Pending |RSA-M09 |Bundle words (new-bundle, bundle-add, bundle-count) -|✅ Done +|🔲 Pending |RSA-M10 |Pack words (new-pack, require-doc, optional-doc) -|✅ Done +|🔲 Pending |RSA-M11 |Validation words (emit-error, emit-warning, emit-suggestion) -|✅ Done +|🔲 Pending |RSA-M12 |Format detection (detect-format) -|✅ Done +|🔲 Pending |RSA-M13 -|WASM bindings for AffineScript -|✅ Done +|Capability-bounded WASM bindings for AffineScript +|🔲 Pending |RSA-M14 |EnforcementBot rule definitions -|✅ Done +|🔲 Pending |RSA-M15 |PackShipper manifest creation -|✅ Done +|🔲 Pending |RSA-M16 |Content hashing (SHA-256) -|✅ Done +|🔲 Pending |RSA-M17 |RSR compliance files @@ -270,7 +279,7 @@ This document defines Must/Should/Could requirements for each component of the d |RSA-S01 |Format parsing words (parse-content, get-headings) -|✅ Done +|🔲 Pending |RSA-S02 |EnforcementBot job scheduling @@ -290,7 +299,7 @@ This document defines Must/Should/Could requirements for each component of the d |RSA-S06 |LogicEngine Datalog rules -|✅ Done +|🔲 Pending |RSA-S07 |Conflict detection algorithms diff --git a/docs/SEAM-CHECK-MUSTS.adoc b/docs/SEAM-CHECK-MUSTS.adoc index f80c897..1830241 100644 --- a/docs/SEAM-CHECK-MUSTS.adoc +++ b/docs/SEAM-CHECK-MUSTS.adoc @@ -8,18 +8,27 @@ == Overview -This document validates integration seams between ecosystem components after implementing all MUST requirements. +WARNING: This is a historical design-time seam report. It does not validate the +current checkouts and must not be used as evidence that the listed components +or seams are implemented. In particular, the current recon-silly-ation state +records its actual reconciliation engine and proposed ForthWall VM as 0% +complete. Every status below requires revalidation against executable code and +independently reproducible tests. == Components Checked -[cols="1,2,1"] +[cols="1,2,2"] |=== -| Component | Repository | Status +| Component | Repository | Historical status (unverified) -| Formatrix Docs | formatrix-docs | ✓ Implemented -| Recon-Silly-Ation | recon-silly-ation | ✓ Implemented -| Docubot | recon-silly-ation/Docubot.res | ✓ Implemented -| Docudactyl | recon-silly-ation/Docudactyl.res | ✓ Implemented +| Formatrix Docs | formatrix-docs | Historical claim: ✓ Implemented; unverified +against current code +| Recon-Silly-Ation | recon-silly-ation | Historical claim: reconciliation +engine not implemented; unverified against current code +| Docubot | recon-silly-ation/Docubot.res | Historical path and status; +unverified against current code +| Docudactyl | recon-silly-ation/Docudactyl.res | Historical path and status; +unverified against current code |=== == SEAM-1: Formatrix ↔ RSA @@ -293,19 +302,25 @@ Different field structures between Protocol.res and Docubot.res. . **SEAM-3D:** Detailed shipping results (string acceptable MVP) . **SEAM-4B:** Multi-approver workflow (single approver acceptable MVP) -== Fixes Applied +== Historical Fixes Claimed (Unverified) + +The statements in this section record what the original report claimed. They +have no dated commit references or independently reproducible evidence and are +not current validation. === Fix 1: Update Rust DocumentEvent -Added `id` and `source` fields (see `commands.rs` update). +Historical claim (unverified): `id` and `source` fields were added (the original +report referred only to a `commands.rs` update). === Fix 2: Update Protocol.res repoContext -Added optional `dependencies` and `readme` fields. +Historical claim (unverified): optional `dependencies` and `readme` fields were +added. === Fix 3: Add Progress Tracking -Will implement in SHOULDs phase. +Historical plan (unverified): implement progress tracking in the SHOULD phase. == Seam Status Matrix diff --git a/docs/V1-PUBLISH-ROADMAP.adoc b/docs/V1-PUBLISH-ROADMAP.adoc index c10ac1c..33870a2 100644 --- a/docs/V1-PUBLISH-ROADMAP.adoc +++ b/docs/V1-PUBLISH-ROADMAP.adoc @@ -10,8 +10,10 @@ This roadmap outlines the path to publishing v1 of the Formatrix documentation ecosystem, comprising four components: -1. **Formatrix Docs** - Human document editor with multi-format tabs -2. **Recon-Silly-Ation** - Document reconciliation engine (ReconForth VM) +1. **Formatrix Docs** - Multi-format viewer being developed into an editor with + synchronised format tabs +2. **Recon-Silly-Ation** - Cross-document consistency reconciler; proposed + bounded rule layer named ForthWall 3. **Docubot** - AI document generation assistant 4. **Docudactyl** - Orchestration and pipeline management @@ -22,15 +24,15 @@ This roadmap outlines the path to publishing v1 of the Formatrix documentation e |Component |Description |MUSTs |SHOULDs |COULDs |Formatrix Docs -|Multi-format document editor -|9/14 (64%) -|6/12 (50%) +|Multi-format viewer; editor under development +|10/14 (71%) +|5/12 (42%) |1/12 (8%) |Recon-Silly-Ation -|ReconForth reconciliation engine -|17/17 (100%) -|2/12 (17%) +|Consistency reconciler with proposed ForthWall rule engine +|1/17 (6%) +|0/12 (0%) |0/8 (0%) |Docubot @@ -46,9 +48,17 @@ This roadmap outlines the path to publishing v1 of the Formatrix documentation e |0/6 (0%) |=== +Formatrix Docs totals are calculated from the current statuses in +`MOSCOW-REQUIREMENTS.adoc`, which is the status source for this roadmap. The +historical seam report is explicitly unverified and is not a readiness source; +a recorded `Done` status supports readiness only with independently reproducible +evidence. + == v1 Minimum Viable Ecosystem -For v1 publish, the following are **required**: +For v1 publish, the following baseline items are **required**. Items labelled +conditional are not v1 commitments unless their stated evidence gates are +adopted as v1 acceptance criteria. === Phase 1: Core Library Completion @@ -58,12 +68,12 @@ For v1 publish, the following are **required**: * [x] C FFI for Ada TUI * [x] ArangoDB document storage * [ ] Document event emission -* [ ] File open/save operations +* [x] File open/save operations -.Recon-Silly-Ation Core -* [x] ReconForth VM complete -* [x] All core word sets -* [x] WASM bindings +.Recon-Silly-Ation Core (conditional v1 scope) +* [ ] ForthWall VM specified, implemented, and proved +* [ ] All core and document word sets implemented and individually evidenced +* [ ] Bounded WASM bindings with capability-confinement tests * [ ] 7-stage pipeline implementation * [ ] CLI interface @@ -80,11 +90,12 @@ For v1 publish, the following are **required**: * [ ] Scheduler * [ ] Health checks -=== Phase 2: GUI/TUI Implementation +=== Phase 2: Viewer and Conditional Editor Implementation .Formatrix GUI (Tauri 2.0 + AffineScript) -* [ ] Basic shell with format tabs -* [ ] Editor component (CodeMirror 6) +* [x] Basic Tauri shell (FD-M07) +* [ ] Format tabs (FD-M08; conditional on editor evidence gates) +* [ ] Editor component (FD-M09; conditional on editor evidence gates) * [ ] File operations dialog * [ ] Status bar with git info @@ -129,17 +140,31 @@ For v1 publish, the following are **required**: == v1 Scope Decisions -=== Included in v1 +=== Baseline Included in v1 -* Basic document editing with format tabs (7 formats) +* Multi-format viewing and conversion through the DocMatrix core (7 formats) * Document storage in ArangoDB with graph links -* ReconForth-based reconciliation with format detection * Basic orchestration for pipeline execution * TUI for terminal-based editing * Container deployment +=== Conditional v1 Scope + +* Synchronized editing (FD-M08 and FD-M09), only if the pending format-tab and + editor capabilities are implemented and independently reproduce cursor + mapping, synchronization, refusal, and undo/redo safety properties +* ForthWall-based reconciliation with format detection, only when the editor + capabilities it composes with have passed those gates and ForthWall's own + capability-confinement, semantic-refusal, replay, and independent-proof gates + have also passed + +These capabilities require explicit composition. If their gates are not +adopted as v1 acceptance criteria, both entries remain in deferred scope. + === Deferred to v2 +* Synchronized editing (FD-M08/FD-M09) and ForthWall reconciliation when their + implementation and proof gates are outside v1 acceptance criteria * Graph visualization (Obsidian-style) * OCR integration (Tesseract) * TTS/STT integration @@ -152,18 +177,18 @@ For v1 publish, the following are **required**: === Immediate (v1-blocking) -1. FD-M06: File open/save operations -2. FD-M07: Tauri GUI shell -3. FD-M09: Editor component -4. DD-M02: Event bus -5. DD-M05: Pipeline executor +1. FD-M12: Document event emission +2. DD-M02: Event bus +3. DD-M05: Pipeline executor === High Priority (v1-desired) -1. FD-M11: Ada TUI -2. RSA-S09: 7-stage pipeline -3. RSA-S11: CLI interface -4. DB-M01-M04: Docubot core (if time permits) +1. FD-M08/FD-M09: Format tabs and editor, conditional on the editor evidence + gates +2. FD-M11: Ada TUI +3. RSA-S09: 7-stage pipeline +4. RSA-S11: CLI interface +5. DB-M01-M04: Docubot core (if time permits) === Nice to Have (post-v1) @@ -205,14 +230,14 @@ docudactyl |Component |Remaining MUSTs |Remaining SHOULDs |Total Effort |Formatrix Docs -|5 items (GUI/TUI focus) -|6 items (GUI-dependent) +|4 items (conditional editor/TUI/event focus) +|7 items (GUI-dependent) |Large |Recon-Silly-Ation -|0 items -|10 items -|Medium +|16 items +|12 items +|Large |Docubot |10 items @@ -227,17 +252,17 @@ docudactyl == Milestones -=== M1: Core Complete (Current) +=== M1: Core Complete (Not Yet Reached) * [x] All format handlers implemented * [x] FFI exports for Ada TUI * [x] ArangoDB client functional -* [x] ReconForth VM operational +* [ ] ForthWall VM operational, capability-confined, and proved === M2: Basic GUI Functional -* [ ] Tauri shell running -* [ ] Format tabs working -* [ ] File open/save working -* [ ] Editor with syntax highlighting +* [x] Basic Tauri shell implemented +* [ ] Format tabs working (conditional FD-M08) +* [x] Core file open/save operations implemented +* [ ] Editor with syntax highlighting (conditional FD-M09) === M3: Basic TUI Functional * [ ] Ada TUI compiling @@ -296,16 +321,20 @@ docudactyl == Next Steps -1. **Immediate**: Implement FD-M06 (file operations) in formatrix-core -2. **This Week**: Set up Tauri shell with basic routing -3. **This Month**: Get format tabs displaying content +1. **Immediate**: Attach independently reproducible evidence to current `Done` + statuses +2. **This Week**: Implement FD-M12 document event emission +3. **Conditional editor scope**: Implement and prove FD-M08/FD-M09 only if their + gates are adopted for v1 4. **Next Month**: TUI implementation parallel to GUI == Success Criteria for v1 -* [ ] User can create, edit, and save documents -* [ ] User can switch between 7 format views +* [ ] User can view, convert, and save documents through the evidenced core +* [ ] If editor scope is accepted, users can edit and switch between 7 format + views only after FD-M08/FD-M09 safety and proof gates pass * [ ] Documents persist in ArangoDB -* [ ] Basic reconciliation works +* [ ] If reconciliation scope is accepted, ForthWall and the editor composition + pass their separate implementation, safety, and proof gates * [ ] Container deployment functional * [ ] Documentation sufficient for onboarding