|
1 | 1 | // SPDX-License-Identifier: PMPL-1.0-or-later |
2 | | -= RSR Template Repository — Show Me The Receipts |
| 2 | += a2ml-haskell — Show Me The Receipts |
| 3 | +Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
3 | 4 | :toc: |
4 | 5 | :icons: font |
5 | 6 |
|
6 | 7 | The README makes claims. This file backs them up. |
7 | 8 |
|
8 | | -[quote, README] |
| 9 | +== A2ML Parser and Renderer for Haskell |
| 10 | + |
| 11 | +[quote, a2ml-haskell.cabal] |
9 | 12 | ____ |
10 | | -The AI reads the manifest, asks you a few questions, and handles everything. |
| 13 | +A Haskell library for parsing and rendering A2ML (Attested Markup Language) |
| 14 | +documents. A2ML is an attestation-native markup format designed for documents |
| 15 | +that carry trust metadata, cryptographic attestations, and provenance |
| 16 | +information. |
11 | 17 | ____ |
12 | 18 |
|
13 | | -== File Map |
| 19 | +`Data.A2ML.Parser` (`src/Data/A2ML/Parser.hs`) implements a line-oriented, |
| 20 | +purely functional parser over `Text`. The top-level function `parseA2ML :: |
| 21 | +Text -> Either ParseError Document` returns a typed sum on failure rather than |
| 22 | +crashing. Internally, `parseLines` → `parseBlocks` recurse over the line list, |
| 23 | +pattern-matching on headings (`# ... #####`), directive blocks |
| 24 | +(`@name: ... @end`), bullet items (`- ` or `* `), and plain paragraph lines. |
| 25 | +Inline formatting (`**bold**`, `*italic*`) is handled by `parseInlines` and |
| 26 | +its helper `parseInlinesItalic` using `Data.Text.breakOn` — no regex, no |
| 27 | +mutable state. |
| 28 | + |
| 29 | +The Haskell variant models directives as multi-line blocks (`@name: ... @end`), |
| 30 | +which is a richer surface syntax than the single-line `@name value` used in |
| 31 | +`a2ml-rs`. Both parsers share the same output semantics for `Document` and |
| 32 | +`Attestation`, but diverge on directives: `Data.A2ML.Types` uses a |
| 33 | +`DirectiveName` sum type with known constructors (`DirAbstract`, `DirRefs`, |
| 34 | +`DirAttestation`, `DirMeta`, `DirCustom Text`) rather than a free-form string pair. |
| 35 | + |
| 36 | +**Caveat:** The inline parser handles `**bold**` and `*italic*` via |
| 37 | +left-to-right `breakOn`; nested or interleaved emphasis (e.g. |
| 38 | +`*a **b** c*`) is not yet handled correctly. Link `[text](url)` and |
| 39 | +`@ref(id)` patterns are declared in the module signature but the parser |
| 40 | +implementation falls through to `PlainText` for those cases — the |
| 41 | +infrastructure is present, the pattern matching is incomplete. The fuzz |
| 42 | +corpus (`tests/fuzz/`) is a placeholder only. |
| 43 | + |
| 44 | +- Parser entry: `src/Data/A2ML/Parser.hs:49` (`parseA2ML :: Text -> Either ParseError Document`) |
| 45 | +- Type definitions: `src/Data/A2ML/Types.hs` (Document, Block, Inline, DirectiveName, Attestation, TrustLevel, Manifest, Reference) |
| 46 | +- Renderer: `src/Data/A2ML/Renderer.hs` |
| 47 | +- Top-level re-export: `src/Data/A2ML.hs` |
| 48 | +- Learn more: https://hackage.haskell.org/package/a2ml (forthcoming), https://github.com/hyperpolymath/a2ml |
| 49 | + |
| 50 | +== Cryptographic Attestation Model |
| 51 | + |
| 52 | +[quote, src/Data/A2ML/Types.hs] |
| 53 | +____ |
| 54 | +A cryptographic attestation attached to content. |
| 55 | +attestationSigner :: Text |
| 56 | +attestationAlgorithm :: Text |
| 57 | +attestationSignature :: Text |
| 58 | +attestationTimestamp :: Maybe Text |
| 59 | +____ |
| 60 | + |
| 61 | +The Haskell `Attestation` type goes further than the Rust binding: it records |
| 62 | +`attestationAlgorithm` (e.g. `"ed25519"`, `"sha256"`) and |
| 63 | +`attestationSignature` (hex or base64). The `TrustLevel` enum runs from |
| 64 | +`Unsigned` through `SelfAttested`, `ThirdPartyAttested`, to `MultiAttested`, |
| 65 | +encoding the number and independence of attestors rather than the Rust |
| 66 | +variant's subjective `Reviewed`/`Verified` distinction. `Manifest` aggregates |
| 67 | +title, author, version, SPDX license, overall `TrustLevel`, and the full |
| 68 | +attestation list — making it the canonical entry point for programmatic |
| 69 | +provenance inspection. |
| 70 | + |
| 71 | +**Caveat:** Signature _verification_ (actual crypto) is not implemented in |
| 72 | +this library. `attestationSignature` is stored as an opaque `Text` field. |
| 73 | +An external verifier (e.g. the `cookie-rebound` Zig FFI layer or a GnuPG |
| 74 | +wrapper) must perform the actual ed25519 or SHA-256 check. The |
| 75 | +`MultiAttested` level is declared but no quorum logic is implemented yet. |
| 76 | + |
| 77 | +- Attestation type: `src/Data/A2ML/Types.hs:96–117` |
| 78 | +- Trust level enum: `src/Data/A2ML/Types.hs:108–117` (Unsigned → MultiAttested) |
| 79 | +- Manifest type: `src/Data/A2ML/Types.hs:119–133` |
| 80 | + |
| 81 | +== Test Suite: Unit, End-to-End, and Property-Based Tests |
| 82 | + |
| 83 | +[quote, a2ml-haskell.cabal] |
| 84 | +____ |
| 85 | +test-suite a2ml-tests |
| 86 | + other-modules: UnitSpec E2ESpec PropertySpec |
| 87 | + build-depends: hspec, QuickCheck, hspec-quickcheck |
| 88 | +____ |
| 89 | + |
| 90 | +The test suite has three modules. `UnitSpec` tests individual parser functions |
| 91 | +in isolation (heading level detection, directive name parsing, bullet list |
| 92 | +accumulation). `E2ESpec` tests the full `parseA2ML` → `renderA2ML` round-trip |
| 93 | +on representative `.a2ml` documents. `PropertySpec` uses QuickCheck to assert |
| 94 | +`parseA2ML (renderA2ML doc) == Right doc` for arbitrary `Document` values — |
| 95 | +that the renderer and parser are mutual inverses. The `Spec.hs` entry point |
| 96 | +aggregates all three via `hspec`. |
| 97 | + |
| 98 | +**Caveat:** QuickCheck generators for `Document` do not yet exercise the full |
| 99 | +surface syntax — generated documents use only headings and paragraphs, not |
| 100 | +directives or attestations. Property coverage will expand as the inline parser |
| 101 | +matures. |
| 102 | + |
| 103 | +- Unit tests: `tests/UnitSpec.hs` |
| 104 | +- End-to-end tests: `tests/E2ESpec.hs` |
| 105 | +- Property tests: `tests/PropertySpec.hs` |
| 106 | +- Test runner: `tests/Spec.hs` |
| 107 | +- Benchmark: `bench/Main.hs` (Criterion for parse and render hot paths) |
| 108 | +- Run: `cabal test` or `just test` |
| 109 | + |
| 110 | +== MPL-2.0 Fallback for Hackage |
| 111 | + |
| 112 | +[quote, a2ml-haskell.cabal] |
| 113 | +____ |
| 114 | +license: MPL-2.0 |
| 115 | +-- (PMPL-1.0-or-later preferred; MPL-2.0 required for Hackage OSI-approved policy) |
| 116 | +____ |
| 117 | + |
| 118 | +Hackage requires an OSI-approved license; PMPL-1.0-or-later is not yet on the |
| 119 | +OSI list. The fallback policy (identical to `a2ml-rs`) applies: every `.hs` |
| 120 | +source file carries the dual SPDX comment. The `LICENSE-MPL-2.0` file is |
| 121 | +present alongside `LICENSE` (PMPL-1.0-or-later). |
| 122 | + |
| 123 | +**Caveat:** Unlike crates.io, Hackage does not enforce the SPDX identifier at |
| 124 | +upload time by machine. The comment in each source file is the enforceable |
| 125 | +record of the dual-license intent. |
| 126 | + |
| 127 | +- License files: `LICENSE` (PMPL-1.0-or-later), `LICENSE-MPL-2.0` |
| 128 | +- Learn more: https://github.com/hyperpolymath/palimpsest-license |
| 129 | + |
| 130 | +== Zig FFI Layer (Scaffold Present, Implementation Pending) |
| 131 | + |
| 132 | +[quote, src/interface/ffi/README.adoc] |
| 133 | +____ |
| 134 | +Zig FFI scaffold for exposing Data.A2ML as a C-ABI library. |
| 135 | +____ |
| 136 | + |
| 137 | +The `src/interface/ffi/` directory follows the standard Idris2-ABI / Zig-FFI |
| 138 | +architecture required by all RSR projects with cross-language interfaces. The |
| 139 | +`build.zig` and `main.zig` scaffold is present; the Idris2 ABI definitions in |
| 140 | +`src/interface/abi/` are placeholders. This layer is intended to expose |
| 141 | +`parseA2ML` as a C-callable function for embedding the Haskell parser in |
| 142 | +non-Haskell hosts via GHC's Foreign Function Interface. |
| 143 | + |
| 144 | +**Caveat:** The Zig FFI is not yet wired to any real Haskell FFI export. No |
| 145 | +`.hs` module currently declares `foreign export ccall` bindings. This is |
| 146 | +scaffolding for a future integration milestone. |
| 147 | + |
| 148 | +- FFI scaffold: `src/interface/ffi/src/main.zig`, `src/interface/ffi/build.zig` |
| 149 | +- ABI placeholder: `src/interface/abi/` (Idris2 definitions pending) |
| 150 | +- Integration test scaffold: `src/interface/ffi/test/integration_test.zig` |
| 151 | + |
| 152 | +== Dogfooded Across The Account |
14 | 153 |
|
15 | 154 | [cols="1,2"] |
16 | 155 | |=== |
17 | | -| Path | What's There |
| 156 | +| Technology | Also Used In |
| 157 | + |
| 158 | +| **A2ML format** | https://github.com/hyperpolymath/a2ml-rs[a2ml-rs] (Rust binding), https://github.com/hyperpolymath/pandoc-a2ml[pandoc-a2ml] (Lua Pandoc reader), https://github.com/hyperpolymath/tree-sitter-a2ml[tree-sitter-a2ml] (grammar) |
| 159 | + |
| 160 | +| **Haskell / GHC + cabal** | https://github.com/hyperpolymath/nextgen-languages[nextgen-languages] (Scaffoldia CLI, AffineScript compiler), https://github.com/hyperpolymath/hypatia[hypatia] (rule modules) |
| 161 | + |
| 162 | +| **hspec + QuickCheck** | Other Haskell projects in the `nextgen-languages` monorepo |
| 163 | + |
| 164 | +| **SPDX MPL-2.0 fallback** | https://github.com/hyperpolymath/a2ml-rs[a2ml-rs] (crates.io policy) |
| 165 | +|=== |
18 | 166 |
|
19 | | -| `src/` | Source code |
20 | | -| `test(s)/` | Test suite |
| 167 | +== File Map |
| 168 | + |
| 169 | +[cols="1,2"] |
21 | 170 | |=== |
| 171 | +| Path | Proves |
22 | 172 |
|
23 | | -== Questions? |
| 173 | +| `src/Data/A2ML.hs` | Top-level re-export module — public API surface for library consumers |
24 | 174 |
|
25 | | -Open an issue or reach out directly — happy to explain anything in more detail. |
| 175 | +| `src/Data/A2ML/Types.hs` | AST: `Document`, `Block`, `Inline`, `DirectiveName` (sum with known values), `Attestation` (with algorithm + signature fields), `TrustLevel` (Unsigned→MultiAttested), `Manifest`, `Reference` |
| 176 | + |
| 177 | +| `src/Data/A2ML/Parser.hs` | Line-oriented parser: `parseA2ML`, `parseA2MLFile`, `ParseError` sum type, `parseBlocks`, `parseInlines`, directive body collector (`collectDirectiveBody`), bullet item accumulator (`collectBulletItems`) |
| 178 | + |
| 179 | +| `src/Data/A2ML/Renderer.hs` | Canonical serialiser: `renderA2ML :: Document -> Text` — intended round-trip inverse of the parser |
| 180 | + |
| 181 | +| `tests/UnitSpec.hs` | Isolated unit tests for parser primitives |
| 182 | + |
| 183 | +| `tests/E2ESpec.hs` | End-to-end parse→render→re-parse round-trip tests |
| 184 | + |
| 185 | +| `tests/PropertySpec.hs` | QuickCheck property: `parse ∘ render = id` for generated documents |
| 186 | + |
| 187 | +| `tests/Spec.hs` | hspec aggregator entry point |
| 188 | + |
| 189 | +| `bench/Main.hs` | Criterion benchmarks for parse and render hot paths |
| 190 | + |
| 191 | +| `a2ml-haskell.cabal` | Package metadata: exposed modules, build deps (base, text, containers, bytestring), GHC warning flags, test suite and benchmark stanza definitions |
| 192 | + |
| 193 | +| `src/interface/abi/` | Idris2 ABI definitions (placeholder — formal proof artefacts pending) |
| 194 | + |
| 195 | +| `src/interface/ffi/src/main.zig` | Zig FFI scaffold for C-ABI exposure of the Haskell parser |
| 196 | + |
| 197 | +| `src/interface/ffi/test/integration_test.zig` | FFI integration test scaffold |
| 198 | + |
| 199 | +| `0-AI-MANIFEST.a2ml` | AI session gatekeeper — read first before modifying any file |
| 200 | + |
| 201 | +| `.machine_readable/` | A2ML state files: STATE.a2ml, ECOSYSTEM.a2ml, META.a2ml |
| 202 | +|=== |
0 commit comments